Skip to main content
JavaScript 热力图教程:构建交互式密度地图
Tutorials

JavaScript 热力图教程:构建交互式密度地图

JavaScript 热力图实战教程:使用 MapAtlas 构建交互式密度地图,涵盖数据格式化、强度加权、颜色渐变,以及人流量分析和配送需求预测等真实场景。

Brent van der Heiden20 min read
#javascript heatmap#heatmap javascript tutorial#interactive heatmap web#data visualization#maps api#geospatial#javascript#density map

热力图能把一大片坐标点变成一眼就能看懂的密度分布。暖色表示事件聚集的区域,冷色表示事件稀疏的区域。用户还没读任何标签,就已经看出了规律。正因如此,热力图成为人流量分析、配送需求预测、犯罪报告工具和房价研究的默认选择。

本教程使用 MapAtlas SDK 从零构建一个交互式热力图。你将把点数据整理为加权 GeoJSON,用自定义颜色渐变渲染密度图层,接入强度滑块,最后完成一个真实的配送需求示例。学完之后,你会得到一套可以直接用于任何 JavaScript 或 React 项目的可复用模式。

如果你刚开始接触 MapAtlas SDK,请先阅读 如何在网站中添加交互式地图。那篇教程介绍了安装、地图初始化和标记,本教程在此基础上继续。

什么时候使用热力图

当你有大量独立的点位,并且想表达的是密度而不是每个点是谁时,热力图就是合适的工具。点数超过几百个后,单个标记就失去了意义,而热力图能呈现出再多图钉也无法展示的结构。

常见用例:

  • 人流量分析:零售选址、城市规划、活动人群建模。为每位进门的顾客记录一个 GPS 定位点,热力图就能显示城市、购物中心或场馆中哪些区域吸引的人最多。
  • 配送需求:按邮编或原始坐标聚合订单来源,向调度员展示需求集中在哪里。这些信息可直接用于区域规划和司机分配。
  • 犯罪与事件数据:警务分析仪表板、保险风险地图和公共安全报告工具都使用密度热力图来传达空间风险,避免大量单个标记让用户应接不暇。
  • 房价梯度:结合加权值后,热力图可以显示城市中房价最高的区域。关于构建房产类地图工具,详见 房产地图教程。

如果你的问题是「事情最常发生在哪里」,答案就是热力图。

数据格式:加权 GeoJSON 点

MapAtlas 热力图图层读取由 Point 要素组成的标准 GeoJSON 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 属性,每个点的贡献相同,热力图反映的就是纯粹的数量密度。

准备工作

开始之前,你需要:

  • MapAtlas API 密钥(免费注册,无需信用卡)
  • Node.js 18+,用于基于 npm 的项目;如果你更喜欢 CDN,也可以使用普通 HTML 页面

第 1 步:安装并初始化地图

安装 SDK:

npm install @mapmetrics/mapmetrics-gl

或者在普通 HTML 文件中通过 CDN 加载:

<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://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/style.json&token=YOUR_API_KEY',
  center: [4.9041, 52.3676], // 阿姆斯特丹
  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' 属性加权(缺省时为 1)
      'heatmap-weight': [
        'interpolate', ['linear'],
        ['get', 'weight'],
        0, 0,
        12, 1
      ],
      // 半径,单位为像素;越大越平滑,但精度越低
      'heatmap-radius': 30,
      // 热力图图层的整体不透明度
      'heatmap-opacity': 0.85,
    },
  });
});

到这一步,你已经有了一个可用的热力图。蓝绿色区域表示低密度,默认渐变在高密度处过渡到黄色和红色。下一步会用一套经过设计的配色替换默认调色板。

第 3 步:自定义颜色渐变

heatmap-color 属性使用 interpolate 表达式把密度值(0 到 1)映射为颜色,这与 MapAtlas 绘制系统其他部分使用的表达式相同。密度为 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)',   // 密度为零时透明
      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)'   // 密度峰值处为亮红色
    ],
  },
});

这个渐变从蓝色(稀疏)经过绿色和黄色过渡到红色(密集),符合大多数用户从天气雷达图和热成像中形成的直觉。如果你的应用有品牌配色,把 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: {
  // ...其他绘制属性...
  'heatmap-intensity': [
    'interpolate', ['linear'],
    ['zoom'],
    8,  1,   // 低缩放:正常强度
    14, 3    // 高缩放:提高强度,补偿点的分散
  ],
  'heatmap-radius': [
    'interpolate', ['linear'],
    ['zoom'],
    8,  20,  // 城市尺度下使用小半径
    14, 50   // 街道尺度下使用较大半径
  ],
},

两个属性都使用基于 zoom 的同一种 interpolate 表达式。色阶之间的值按线性插值计算,所以用户缩放时过渡是平滑的。

真实示例:配送需求热力图

下面是一个配送需求仪表板的完整、独立实现。订单以 GeoJSON FeatureCollection 的形式从 API 获取,用户每次更改时间筛选时,热力图都会更新。

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://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/style.json&token=${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(); // 返回 GeoJSON FeatureCollection
}

map.on('load', async () => {
  const orders = await loadOrders(8, 12); // 早高峰

  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,
    },
  });

  // 按时段筛选的按钮
  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 定位点,不要把它们全部发送到浏览器。在服务器端做一次空间聚合(H3 六边形网格、四叉树或简单的网格取整),把 100 万个原始点压缩成 10,000 个带 count 和 weight 字段的网格单元。在常用缩放级别下,热力图看起来几乎相同,加载时间却只需原来的一小部分。

增量推送更新。 对于实时数据(实时人流量、实时下单),使用 setData 配合最近数据点的滑动窗口,而不是不断累积一个越来越大的 GeoJSON 对象。把数据源保持在固定的最大点数,并淘汰旧记录。

在数据源上设置 maxzoom。 在 addSource 调用中加入 maxzoom: 14,SDK 在缩放级别 14 以上就会停止请求切片数据。对热力图来说影响通常不大,因为热力图图层读取的是单个扁平的 GeoJSON 数据源,而不是切片数据,但它可以避免在高缩放级别下进行不必要的重复处理。

保持绘制表达式简单。 heatmap-color 渐变在设置时会被一次性烘焙成颜色渐变纹理,因此增加颜色色阶几乎不会带来每帧开销。在移动端,较大的 heatmap-radius 和较多的点数才是主要开销,应优先调整这两项。

延迟初始化地图。 把整个地图初始化过程放进 IntersectionObserver 回调中,只有当地图容器滚动进入视口时才执行。这样 SDK 包就不会占用首屏加载,对于地图位于首屏以下的营销页面尤其有价值。

如需深入了解地图性能模式,包括懒加载和聚类,可参考 如何在网站中添加交互式地图 中的生产环境检查清单。

React 集成

把热力图封装成 React 组件的方式与任何 MapAtlas 地图相同:在 useEffect 中初始化,通过 useState 管理滑块和筛选状态,并在组件卸载时清理。

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://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/style.json&token=${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]);

  // 更新半径,无需重新挂载地图
  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>
  );
}

在 Next.js 中,使用 dynamic(() => import('./DeliveryHeatmap'), { ssr: false }) 导入该组件,以避免这个仅限浏览器的 SDK 在服务端渲染时报错。

下一步可以构建什么

现在你已经拥有一个可用的交互式热力图,具备加权数据、自定义颜色渐变、半径滑块和随缩放调整的强度。接下来可以这样扩展:

  • 在热力图上叠加 路线图层,把计划的配送路线与需求分布放在一起展示。路线优化 API 教程 介绍了完整实现。
  • 添加 时间动画:用 setInterval 循环按小时切换快照,并在数据源上调用 setData。这会把静态密度地图变成展示需求在一天中如何流动的延时动画。
  • 结合 房价数据,按街区展示价格梯度。房产地图教程 介绍了房产平台的加权数据模式。
  • 查看 MapAtlas 价格页面,为你的生产流量选择合适的套餐。

完整的 SDK 参考和更多示例,请访问 docs.mapatlas.xyz。

常见问题

热力图和分级统计图有什么区别?

热力图通过把相邻点融合成连续的颜色渐变来展示点的密度,适合 GPS 定位点或事件位置这类原始坐标数据。分级统计图(choropleth)则按统计值为预先划定的地理区域(国家、邮编区、人口普查区)着色。数据是大量独立点时用热力图,数据已经按区域聚合时用分级统计图。

JavaScript 热力图能处理多少个数据点?

MapAtlas 热力图图层通过 WebGL 在 GPU 上渲染,在常见硬件上可以流畅处理数万个点。对于规模大得多的数据集,例如数十万个点及以上,建议在服务器端把数据预聚合为较低分辨率的网格,并改用 GeoJSON fill-extrusion 或 circle 图层,这样在低端设备上性能更好。

可以免费使用 MapAtlas 热力图吗?

可以。MapAtlas 提供免费套餐,包含地图瓦片渲染、GeoJSON 图层支持和热力图图层。免费套餐适用于开发阶段和低流量的生产环境。完整套餐详情见 mapatlas.eu/pricing。

热力图在移动端浏览器上能用吗?

能。MapAtlas SDK 使用 WebGL 渲染,所有现代移动端浏览器都支持,包括 iOS 上的 Safari 和 Android 上的 Chrome。在低端移动设备上处理超大数据集时,减少点的数量或增大热力图半径,可以保持帧率流畅。


相关阅读:

常见问题

热力图和分级统计图有什么区别?

热力图通过把相邻点融合成连续的颜色渐变来展示点的密度,适合 GPS 定位点或事件位置这类原始坐标数据。分级统计图(choropleth)则按统计值为预先划定的地理区域(国家、邮编区、人口普查区)着色。数据是大量独立点时用热力图,数据已经按区域聚合时用分级统计图。

JavaScript 热力图能处理多少个数据点?

MapAtlas 热力图图层通过 WebGL 在 GPU 上渲染,在常见硬件上可以流畅处理数万个点。对于规模大得多的数据集,例如数十万个点及以上,建议在服务器端把数据预聚合为较低分辨率的网格,并改用 GeoJSON fill-extrusion 或 circle 图层,这样在低端设备上性能更好。

可以免费使用 MapAtlas 热力图吗?

可以。MapAtlas 提供免费套餐,包含地图瓦片渲染、GeoJSON 图层支持和热力图图层。免费套餐适用于开发阶段和低流量的生产环境。完整套餐详情见 mapatlas.eu/pricing。

热力图在移动端浏览器上能用吗?

能。MapAtlas SDK 使用 WebGL 渲染,所有现代移动端浏览器都支持,包括 iOS 上的 Safari 和 Android 上的 Chrome。在低端移动设备上处理超大数据集时,减少点的数量或增大热力图半径(在视觉上合并更多点),可以保持帧率流畅。

觉得有用?分享给他人吧。

关于作者

Brent van der Heiden

作者

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

查看所有文章 →
返回博客