Skip to main content
آموزش نقشه حرارتی JavaScript: ساخت نقشه چگالی تعاملی در ۲۰۲۶
Tutorials

آموزش نقشه حرارتی JavaScript: ساخت نقشه چگالی تعاملی در ۲۰۲۶

آموزش نقشه حرارتی JavaScript: ساخت نقشه چگالی تعاملی با MapAtlas. قالب‌بندی داده، وزن‌دهی شدت، گرادیان‌های رنگی و کاربردهای واقعی مثل تحلیل تردد پیادگان و تقاضای تحویل.

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

نقشه‌ی حرارتی ابری از نقاط مختصات را به سطح چگالی فوری‌خوان تبدیل می‌کند. رنگ‌های داغ جایی را نشان می‌دهند که رویدادها خوشه‌ای می‌شوند؛ رنگ‌های خنک جایی را نشان می‌دهند که نازک‌شان می‌شوند. کاربران نمونه را قبل از خواندن برچسب درک می‌کنند. این لی نقشه‌های حرارتی گزینه‌ی پیش‌فرض برای تحلیل تردد پیادگان، پیش‌بینی تقاضای تحویل، ابزارهای گزارش جرم و تحقیق قیمت ملک است.

این آموزش نقشه‌ی حرارتی تعاملی را از ابتدا با استفاده از MapAtlas SDK می‌سازد. داده‌های نقطه را به‌عنوان GeoJSON وزن‌دار‌شده ساختار‌دهید، لایه‌ی چگالی را با گرادیان رنگ سفارشی رندر کنید، slider شدت را کابل‌کشی کنید و با مثال تقاضای تحویل دنیای واقعی پایان دهید. در پایان یک الگوی قابل‌استفاده‌ی مجدد خواهید داشت که می‌توانید در هر پروژه‌ی JavaScript یا React بریزید.

اگر تازه برای MapAtlas SDK هستید، ابتدا How to Add Interactive Maps to Your Website را بخوانید. این نصب، اولیه‌سازی نقشه و علامت‌ها را پوشش می‌دهد. این آموزش آنجایی را که آن تمام شده بر می‌دارد.

هنگام استفاده از نقشه‌ی حرارتی

نقشه‌های حرارتی ابزار درست هستند هنگامی که مجموعه‌ی بزرگی از محل‌های نقطه‌ی انفرادی دارید و می‌خواهید چگالی را برقرار کنید، نه هویت. علامت‌های انفرادی معنی‌ای فراتر از چند‌صد نقطه را از دست می‌دهند؛ نقشه‌ی حرارتی ساختاری را نشان می‌دهد که هیچ میزان سنجه نمی‌تواند.

موارد استفاده‌ی معمول:

  • تحلیل تردد پیادگان: انتخاب سایت خرده‌فروشی، برنامه‌ریزی شهری، مدل‌سازی جمعیت رویداد. برای هر مشتری که درب را پشت سر گذاشت GPS ping بریزید و نقشه‌ی حرارتی نشان می‌دهد کدام مناطق شهری، مرکز خریدی یا مکان بیشترین مردم را می‌کشانند.
  • تقاضای تحویل: منشا سفارش را براساس کدپستال یا مختصات خام کل‌کنید تا به راننده‌ها نشان دهید کجا تقاضا متمرکز است. این مستقیما به برنامه‌ریزی منطقه و اختصاص راننده تغذیه می‌کند.
  • داده‌های جرم و حادثه: داشبورد‌های تجزیه‌ی شرطه، نقشه‌های ریسک بیمه و ابزارهای گزارش ایمنی عمومی همه از نقشه‌های حرارتی چگالی برای برقراری ریسک مکانی بدون غلبه‌ی کاربرها با علامت‌های انفرادی استفاده می‌کنند.
  • گرادیان‌های قیمت ملک: هنگام ترکیب با مقادیر وزن‌دار، نقشه‌ی حرارتی می‌تواند نشان دهد کجا قیمت‌ها در سراسر شهر بالاترین است. برای اطلاعات بیشتر درباره‌ی ساخت ابزارهای نقشه‌ی متمرکز برملک Real Estate Property Map tutorial را ببینید.

اگر سؤال شما «در کجا بیشتر رویداد رخ می‌دهد؟» است، نقشه‌ی حرارتی پاسخ شماست.

قالب داده: نقاط GeoJSON وزن‌دار

لایه‌ی نقشه‌ی حرارتی MapAtlas FeatureCollection استاندارد GeoJSON از ویژگی‌های Point را می‌خواند. هر ویژگی می‌تواند مالکیت weight را حمل کند که سهم آن را برای سطح چگالی مقیاس می‌کند. سفارش تحویلی با ارزش ۱۰ مورد بیشتر گرمای نسبت به سفارش تک‌مورده ایجاد می‌کند؛ شاخهه‌ی حمل‌ونقل اصلی تردد پیادگان بیشتری نسبت به خیابان کناری ایجاد می‌کند.

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 بدون‌بعد است. آن را نسبت به محدوده‌ی دادگان خود عادی‌سازی می‌کنید. اگر منطقه‌ی بالاترین تقاضای شما ۵۰۰ سفارش تولید کند و آرام‌ترین منطقه ۱۰ تولید کند، آنها را قبل از ارسال به مقیاس ۱ تا ۱۰ نگاشت کنید. این نقشه‌ی حرارتی را بصری‌معنی‌بخش نسبت به دادگان‌های بسیار متفاوت حفظ می‌کند.

بدون مالکیت weight، هر نقطه برابر سهم ایجاد می‌کند و نقشه‌ی حرارتی چگالی شمار خالص را منعکس می‌کند.

پیش‌نیازها

قبل از شروع:

  • کلید MapAtlas API (رایگان ثبت‌نام کنید، بدون کارت اعتباری)
  • Node.js 18+ برای پروژه‌های مبتنی بر npm یا صفحه‌ی ساده‌ی HTML اگر CDN را ترجیح دهید

مرحله‌ی ۱: نصب و اولیه‌سازی نقشه

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>

نقشه را اولیه‌سازی کنید. سبک Dark باعث می‌شود گرادیان‌های رنگ نقشه‌ی حرارتی در برابر زمینه بیرون‌آیند، لی گزینه‌ی ترجیح‌داده برای تصور چگالی است:

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

مرحله‌ی ۲: افزودن لایه‌ی نقشه‌ی حرارتی

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' وزن‌دهید (اگر غایب باشد ۱ را تعریف می‌کند)
      'heatmap-weight': [
        'interpolate', ['linear'],
        ['get', 'weight'],
        0, 0,
        12, 1
      ],
      // شعاع در پیکسل؛ بزرگتر = صاف‌تر اما کم‌دقیق‌تر
      'heatmap-radius': 30,
      // کدری کلی لایه‌ی نقشه‌ی حرارتی
      'heatmap-opacity': 0.85,
    },
  });
});

در این مرحله نقشه‌ی حرارتی کاری دارید. مناطق آبی‌سبز چگالی پایین هستند؛ گرادیان پیش‌فرض نسبت‌مند زرد و قرمز در چگالی بالا. مرحله‌ی بعد پالت پیش‌فرض را با طرح‌ی رنگ عمدی جایگزین می‌کند.

مرحله‌ی ۳: سفارشی‌سازی گرادیان رنگ

مالکیت heatmap-color مقادیر چگالی (۰ تا ۱) را با استفاده از بیان interpolate یکسان نقشه‌ی رنگ سیستم MapAtlas نگاشت می‌کند. چگالی ۰ شفاف است تا لایه‌ی بنیاد نقشه در مناطق خالی نشان دهد.

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 را با تمام‌نقاط خودتان جایگزین کنید؛ درون‌یابی انتقال‌های صاف را خودکار مدیریت می‌کند.

نکته‌ی طراحی: برای داشبورد‌های حرفه‌ای و ابزارهای تجزیه‌ی داده، انتهای چگالی‌پایین را تقریبا شفاف نگاه‌دارید. این خوانایی نقشه‌ی بنیاد در مناطق آرام را حفظ می‌کند و چشم را طبیعی‌انگیزه به مناطق داغ می‌کشاند.

Step 4: Adding a Radius Slider for Interactive Control

Heatmap radius controls how far each point "radiates" influence. A small radius (10 to 15px) shows fine-grained clusters; a large radius (50 to 80px) produces a broader, smoother surface. Different use cases call for different defaults: foot traffic in a dense city centre needs a small radius, national delivery demand across a whole country needs a large one.

Give users a slider so they can adjust on the fly:

<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>

Wire the slider to setPaintProperty, which updates the layer without re-adding it:

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 is a live update with no flicker. The heatmap re-renders on the GPU in the same frame. This pattern works for any paint property: opacity, intensity, colour stops.

Step 5: Intensity Scaling by Zoom Level

At low zoom levels (city-wide view), nearby points overlap heavily and the heatmap can look uniformly saturated. At high zoom (street level), the same points spread out and the density surface looks sparse. Zoom-linked intensity compensation keeps the visualisation readable at every zoom.

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
  ],
},

Both properties use the same interpolate over zoom expression. The values between the stops are interpolated linearly, so the transition is smooth as the user zooms.

Real-World Example: Delivery Demand Heatmap

Here is a complete, self-contained implementation for a delivery demand dashboard. Orders arrive from an API as a GeoJSON FeatureCollection. The heatmap updates whenever the user changes the time filter.

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

The setData call on an existing source replaces the GeoJSON without re-adding the layer. The heatmap re-renders automatically. This pattern scales to any time-based filter: hour of day, day of week, weather condition.

For the route planning layer that typically accompanies a delivery demand heatmap, see the Route Optimization API tutorial. Overlaying an optimised delivery route on top of a demand heatmap gives dispatchers a complete operational picture in a single view.

Performance Tips for Large Datasets

The MapAtlas heatmap layer uses WebGL and renders fast, but the data pipeline feeding it can become a bottleneck at scale.

Pre-aggregate on the server for very large datasets. If you have millions of GPS pings, do not send them all to the browser. Run a server-side spatial aggregation (H3 hexagonal grid, quadtree, or a simple grid rounding) that reduces your 1 million raw points to 10,000 grid cells with a count and weight field. The heatmap will look identical to the user and will load in a fraction of the time.

Stream updates incrementally. For live data (real-time foot traffic, live order placement), use setData with a rolling window of recent points rather than accumulating an ever-growing GeoJSON object. Keep the source at a fixed maximum point count and evict old records.

Use a maxzoom on the source. Adding maxzoom: 14 to your addSource call tells the SDK to stop requesting tile data above zoom 14. For heatmaps this rarely matters since heatmap layers read a single flat GeoJSON source rather than tiled data, but it prevents unnecessary re-processing on high zoom levels.

Reduce paint property complexity. Each additional interpolate stop in a paint expression adds GPU evaluation cost per frame. For mobile-targeted apps, simplify the colour gradient to three or four stops and drop the zoom-linked radius/intensity scaling on lower-priority views.

Lazy-initialise the map. Wrap the entire map initialisation in an IntersectionObserver callback so it only runs when the map container scrolls into view. This defers the SDK bundle from the initial page load and is especially valuable on marketing pages where the map is below the fold.

For a deeper dive on map performance patterns, including lazy loading and clustering, the production checklist in How to Add Interactive Maps to Your Website covers the full list.

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 setInterval loop calling setData on 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:

این مفید بود؟ آن را به اشتراک بگذارید.

درباره نویسنده

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. He writes about geospatial infrastructure, AI search visibility, and how location data powers the products people rely on every day.

مشاهده همه مقالات
بازگشت به وبلاگ