كيفية بناء خريطة حرارية تفاعلية بـ JavaScript (2026)
Tutorial

كيفية بناء خريطة حرارية تفاعلية بـ JavaScript (2026)

برنامج تعليمي خطوة بخطوة: ابنِ خريطة حرارية للكثافة بـ JavaScript باستخدام MapAtlas. يغطي تنسيق البيانات وترجيح الشدة وتدرجات الألوان وحالات الاستخدام الواقعية مثل حركة المشاة والطلب على التوصيل.

MapAtlas Team13 min read
#javascript heatmap#heatmap javascript tutorial#interactive heatmap web#data visualization#maps api#geospatial#javascript#density map

A heatmap turns a cloud of coordinate points into an instantly readable density surface. Hot colours mark where events cluster; cool colours mark where they thin out. Users grasp the pattern before they read a single label. That is why heatmaps are the default choice for foot traffic analysis, delivery demand forecasting, crime reporting tools, and property price research.

This tutorial builds an interactive heatmap from scratch using the MapAtlas SDK. You will structure point data as weighted GeoJSON, render a density layer with a custom colour gradient, wire up an intensity slider, and finish with a real-world delivery demand example. By the end you will have a reusable pattern you can drop into any JavaScript or React project.

If you are new to the MapAtlas SDK, read How to Add Interactive Maps to Your Website first. It covers installation, map initialisation, and markers. This tutorial picks up where that one leaves off.

When to Use a Heatmap

Heatmaps are the right tool when you have a large set of individual point locations and want to communicate density, not identity. Individual markers lose meaning past a few hundred points; a heatmap reveals structure that no amount of pinning could.

Common use cases:

  • Foot traffic analysis: retail site selection, urban planning, event crowd modelling. Drop a GPS ping for every customer who passed through a door and the heatmap shows which areas of a city, shopping centre, or venue draw the most people.
  • Delivery demand: aggregate order origins by postcode or raw coordinate to show dispatchers where demand is concentrated. This feeds directly into zone planning and driver allocation.
  • Crime and incident data: police analytics dashboards, insurance risk maps, and public safety reporting tools all use density heatmaps to communicate spatial risk without overwhelming users with individual markers.
  • Property price gradients: when combined with weighted values, a heatmap can show where prices are highest across a city. See the Real Estate Property Map tutorial for more on building property-focused map tools.

If your question is "where do things happen most?" a heatmap is your answer.

Data Format: Weighted GeoJSON Points

The MapAtlas heatmap layer reads a standard GeoJSON FeatureCollection of Point features. Each feature can carry a weight property that scales its contribution to the density surface. A delivery order worth 10 items contributes more heat than a single-item order; a major transit hub generates more foot traffic than a side street.

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" }
    }
  ]
};

The weight value is dimensionless. You normalise it relative to your dataset's range. If your highest-demand zone generates 500 orders and your quietest generates 10, map those to a 1 to 10 scale before passing them in. This keeps the heatmap visually meaningful across datasets with very different absolute counts.

Without a weight property, every point contributes equally and the heatmap reflects pure count density.

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

Step 1: Install and Initialise the Map

Install the SDK:

npm install @mapmetrics/mapmetrics-gl

Or load it via CDN in a plain HTML file:

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

Add a container with a defined height:

<div id="map" style="width: 100%; height: 600px;"></div>

Initialise the map. The Dark style makes heatmap colour gradients pop against the background, which is why it is the preferred base for density visualisations:

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

Step 2: Add the Heatmap Layer

Register your GeoJSON as a source, then add a heatmap layer that reads from it. The map.on('load', ...) callback ensures the style has finished loading before you modify it.

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

At this point you have a working heatmap. Blue-green areas are low density; the default gradient shifts toward yellow and red at high density. The next step replaces the default palette with a deliberate colour scheme.

Step 3: Customising the Colour Gradient

The heatmap-color property maps density values (0 to 1) to colours using the same interpolate expression used across the rest of the MapAtlas paint system. Density 0 is transparent so the map base layer shows through in empty areas.

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

This gradient runs blue (sparse) through green and yellow to red (dense), matching the mental model most users bring from weather radar maps and thermal imaging. If your app has a branded colour palette, replace the RGB values with your own stops; the interpolation handles smooth transitions automatically.

Design tip: For professional dashboards and analytics tools, keep the low-density end nearly transparent. It preserves the readability of the base map in quiet areas and draws the eye naturally to the hot zones.

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:

وجدت هذا مفيداً؟ شاركه.

Back to Blog