Eine Heatmap verwandelt eine Wolke von Koordinaten in eine sofort lesbare Dichte-Oberfläche. Warme Farben markieren, wo Events clustern; kühle Farben markieren, wo sie sich ausdünnen. Benutzer erfassen das Muster, bevor sie ein einziges Label lesen. Deshalb sind Heatmaps die Standardwahl für Fußgängerfrequenz-Analyse, Liefernachfrage-Prognosen, Kriminalitäts-Meldungstools und Immobilien-Preisforschung.
Dieses Tutorial erstellt eine interaktive Heatmap von Grund auf mit dem MapAtlas SDK. Sie strukturieren Punktdaten als gewichtete GeoJSON, rendern eine Dichte-Schicht mit einem benutzerdefinierten Farbverlauf, verdrahten einen Intensitäts-Schieber und beenden mit einem realen Liefernachfrage-Beispiel. Am Ende haben Sie ein wiederverwendbares Muster, das Sie in jedes JavaScript oder React-Projekt einfügen können.
Wenn Sie neu beim MapAtlas SDK sind, lesen Sie zuerst How to Add Interactive Maps to Your Website. Es behandelt Installation, Map-Initialisierung und Marker. Dieses Tutorial setzt dort an, wo das andere aufhört.
Wann man eine Heatmap verwenden sollte
Heatmaps sind das richtige Werkzeug, wenn Sie eine große Menge von einzelnen Punkt-Standorten haben und Dichte kommunizieren möchten, nicht Identität. Einzelne Marker verlieren Bedeutung über mehrere Hundert Punkte hinweg; eine Heatmap offenbart Struktur, die keine Menge Pinning könnte.
Häufige Anwendungsfälle:
- Fußgängerfrequenz-Analyse: Einzelhandelsstandort-Auswahl, Stadtplanung, Event-Menschenmenge-Modellierung. Für jeden Kunden, der eine Tür passierte, ein GPS-Ping hinterlassen und die Heatmap zeigt, welche Gebiete einer Stadt, eines Einkaufszentrums oder eines Veranstaltungsortes die meisten Menschen anziehen.
- Liefernachfrage: Aggregieren Sie Bestellursprünge nach Postleitzahl oder Rohkoordinate, um Dispatchern zu zeigen, wo die Nachfrage konzentriert ist. Dies speist direkt in Zonenplanung und Fahrerverteilung.
- Kriminalitäts- und Incident-Daten: Polizei-Analytics-Dashboards, Versicherungs-Risiko-Karten und öffentliche Sicherheits-Meldungstools verwenden alle Dichte-Heatmaps, um Raumsicherheitsrisiken zu kommunizieren, ohne Benutzer mit einzelnen Markern zu überfordern.
- Immobilien-Preis-Gradienten: In Kombination mit gewichteten Werten kann eine Heatmap zeigen, wo Preise in einer Stadt am höchsten sind. Siehe das Real Estate Property Map-Tutorial für mehr zur Erstellung von Immobilien-fokussierten Map-Tools.
Wenn Ihre Frage "wo passieren Dinge am meisten?" lautet, ist eine Heatmap Ihre Antwort.
Datenformat: Gewichtete GeoJSON-Punkte
Die MapAtlas Heatmap-Schicht liest eine Standard-GeoJSON FeatureCollection von Point-Funktionen. Jede Funktion kann eine weight-Eigenschaft tragen, die ihren Beitrag zur Dichte-Oberfläche skaliert. Eine Lieferbestellung mit 10 Artikeln trägt mehr Wärme bei als eine Bestellung mit einem Artikel; ein großes Transit-Hub erzeugt mehr Fußgängerverkehr als eine Seitenstraße.
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" }
}
]
};
Der weight-Wert ist dimensionslos. Sie normalisieren ihn relativ zum Bereich Ihres Datensatzes. Wenn Ihre Zone mit der höchsten Nachfrage 500 Bestellungen erzeugt und Ihre ruhigste 10 erzeugt, ordnen Sie diese vor der Übergabe einer Skala von 1 bis 10 zu. Dies hält die Heatmap visuell bedeutsam über Datensätze mit sehr unterschiedlichen absoluten Zählungen hinweg.
Ohne weight-Eigenschaft trägt jeder Punkt gleich bei und die Heatmap widerspiegelt reine Zähldichte.
Voraussetzungen
Bevor Sie beginnen:
- Ein MapAtlas API-Schlüssel (kostenlos anmelden, keine Kreditkarte erforderlich)
- Node.js 18+ für npm-basierte Projekte oder eine plain HTML-Seite, wenn Sie das CDN bevorzugen
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-Tipps für große Datensätze
Die MapAtlas Heatmap-Schicht verwendet WebGL und wird schnell gerendert, aber die Daten-Pipeline, die es speist, kann bei Skalierung ein Engpass werden.
Pre-aggregate auf dem Server für sehr große Datensätze. Wenn Sie Millionen von GPS-Pings haben, senden Sie nicht alle zum Browser. Führen Sie eine server-seitige räumliche Aggregation aus (H3 hexagonales Gitter, Quadtree oder einfaches Gitter-Rounding), die Ihre 1 Million Rohpunkte auf 10.000 Gitterzellen mit count- und weight-Feld reduziert. Die Heatmap sieht für den Benutzer identisch aus und wird in einem Bruchteil der Zeit geladen.
Stream-Updates inkrementell. Für Live-Daten (Echtzeit-Fußgängerfrequenz, Live-Bestellungsplatzierung) verwenden Sie setData mit einem rollierenden Fenster neuerer Punkte, anstatt ein ständig wachsendes GeoJSON-Objekt anzusammeln. Halten Sie die Quelle bei einer festen maximalen Punktzahl und entfernen Sie alte Datensätze.
Verwenden Sie maxzoom auf der Quelle. Das Hinzufügen von maxzoom: 14 zu Ihrem addSource-Aufruf teilt dem SDK mit, dass es über Zoom 14 hinaus keine Tile-Daten mehr anfordert. Für Heatmaps spielt dies selten eine Rolle, da Heatmap-Schichten eine einzelne flache GeoJSON-Quelle anstelle von gekachelten Daten lesen, aber es verhindert unnötige Neuverzarbeitung bei hohen Zoomstufen.
Reduzieren Sie die Komplexität der Paint-Eigenschaft. Jeder zusätzliche interpolate-Stopp in einem Paint-Ausdruck erhöht die GPU-Bewertungskosten pro Frame. Für Mobile-gezielte Apps vereinfachen Sie den Farbverlauf auf drei oder vier Stopps und lassen Sie zoom-verknüpfte Radius-/Intensitätsskalierung auf niedrigeren Priorität-Ansichten fallen.
Lazy-Initialisieren Sie die Karte. Wickeln Sie die gesamte Map-Initialisierung in einen IntersectionObserver-Rückruf ein, damit sie nur läuft, wenn der Map-Container in Sicht rollt. Dies verzögert das SDK-Bundle vom anfänglichen Seitenladung und ist besonders wertvoll auf Marketing-Seiten, bei denen die Karte unterhalb der Falte ist.
Für einen tieferen Einstieg in Map-Performance-Muster, einschließlich Lazy-Laden und Clustering, der Production-Checkliste in How to Add Interactive Maps to Your Website deckt die vollständige Liste ab.
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:

