Een heatmap maakt van een wolk coördinaten een dichtheidsoppervlak dat je in één oogopslag leest. Warme kleuren markeren waar gebeurtenissen clusteren, koele kleuren waar ze uitdunnen. Gebruikers zien het patroon voordat ze ook maar één label lezen. Daarom zijn heatmaps de standaardkeuze voor analyse van bezoekersstromen, het voorspellen van bezorgvraag, tools voor misdaadrapportage en onderzoek naar vastgoedprijzen.
In deze tutorial bouw je vanaf nul een interactieve heatmap met de MapAtlas SDK. Je structureert puntdata als gewogen GeoJSON, rendert een dichtheidslaag met een eigen kleurverloop, koppelt een intensiteitsslider en sluit af met een praktijkvoorbeeld over bezorgvraag. Aan het eind heb je een herbruikbaar patroon dat je in elk JavaScript- of React-project kunt inzetten.
Ben je nieuw met de MapAtlas SDK, lees dan eerst Hoe voeg je interactieve kaarten toe aan je website. Dat artikel behandelt de installatie, het initialiseren van de kaart en markers. Deze tutorial gaat verder waar dat artikel stopt.
Wanneer gebruik je een heatmap
Heatmaps zijn het juiste middel als je een grote set losse puntlocaties hebt en dichtheid wilt laten zien, geen identiteit. Losse markers verliezen hun betekenis boven een paar honderd punten. Een heatmap maakt structuur zichtbaar die je met pins nooit zou krijgen.
Veelvoorkomende use cases:
- Analyse van bezoekersstromen: locatiekeuze in retail, stedelijke planning, modellering van menigten bij evenementen. Leg een GPS-ping vast voor elke klant die een deur passeerde, en de heatmap laat zien welke delen van een stad, winkelcentrum of evenementlocatie de meeste mensen trekken.
- Bezorgvraag: aggregeer de herkomst van bestellingen per postcode of per ruwe coördinaat, zodat dispatchers zien waar de vraag geconcentreerd is. Dat voedt direct de zoneplanning en de inzet van chauffeurs.
- Misdaad- en incidentdata: analysedashboards van de politie, risicokaarten van verzekeraars en tools voor openbare veiligheid gebruiken allemaal dichtheidsheatmaps om ruimtelijk risico te tonen zonder gebruikers te overspoelen met losse markers.
- Prijsverloop van vastgoed: gecombineerd met gewogen waarden laat een heatmap zien waar de prijzen in een stad het hoogst zijn. Zie de tutorial Real Estate Property Map voor meer over het bouwen van kaarttools voor vastgoed.
Is je vraag "waar gebeurt het het vaakst?", dan is een heatmap het antwoord.
Dataformaat: gewogen GeoJSON-punten
De heatmaplaag van MapAtlas leest een standaard GeoJSON FeatureCollection met Point-features. Elke feature kan een weight-property hebben die bepaalt hoeveel die feature bijdraagt aan het dichtheidsoppervlak. Een bestelling van 10 artikelen levert meer warmte op dan een bestelling van één artikel, en een groot vervoersknooppunt trekt meer voetgangers dan een zijstraat.
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" }
}
]
};
De waarde van weight heeft geen eenheid. Je normaliseert hem ten opzichte van het bereik van je dataset. Levert je drukste zone 500 bestellingen op en je rustigste 10, zet die dan om naar een schaal van 1 tot 10 voordat je ze doorgeeft. Zo blijft de heatmap visueel betekenisvol, ook bij datasets met sterk verschillende absolute aantallen.
Zonder weight-property telt elk punt even zwaar en toont de heatmap puur de dichtheid in aantallen.
Vereisten
Voordat je begint:
- Een MapAtlas API-key (gratis aanmelden, geen creditcard nodig)
- Node.js 18+ voor projecten met npm, of een gewone HTML-pagina als je liever de CDN gebruikt
Stap 1: installeer en initialiseer de kaart
Installeer de SDK:
npm install @mapmetrics/mapmetrics-gl
Of laad hem via de CDN in een gewoon HTML-bestand:
<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>
Voeg een container met een vaste hoogte toe:
<div id="map" style="width: 100%; height: 600px;"></div>
Initialiseer de kaart met een donkere stijl uit je account. Op een donkere basis springen de kleurverlopen van de heatmap eruit, daarom is dit de voorkeursbasis voor dichtheidsvisualisaties:
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], // Amsterdam
zoom: 12,
});
Stap 2: voeg de heatmaplaag toe
Registreer je GeoJSON als source en voeg daarna een heatmaplaag toe die daaruit leest. De callback map.on('load', ...) zorgt ervoor dat de stijl volledig geladen is voordat je hem aanpast.
map.on('load', () => {
map.addSource('demand', {
type: 'geojson',
data: demandData,
});
map.addLayer({
id: 'demand-heatmap',
type: 'heatmap',
source: 'demand',
paint: {
// Weeg elk punt op zijn 'weight'-property (standaard 1 als die ontbreekt)
'heatmap-weight': [
'interpolate', ['linear'],
['get', 'weight'],
0, 0,
12, 1
],
// Straal in pixels; groter = vloeiender maar minder precies
'heatmap-radius': 30,
// Totale dekking van de heatmaplaag
'heatmap-opacity': 0.85,
},
});
});
Je hebt nu een werkende heatmap. Blauwgroene gebieden hebben een lage dichtheid, en het standaardverloop schuift bij hoge dichtheid op naar geel en rood. In de volgende stap vervang je het standaardpalet door een bewust gekozen kleurenschema.
Stap 3: pas het kleurverloop aan
De property heatmap-color koppelt dichtheidswaarden (0 tot 1) aan kleuren met dezelfde interpolate-expressie die in de rest van het paint-systeem van MapAtlas wordt gebruikt. Dichtheid 0 is transparant, zodat de basiskaart in lege gebieden zichtbaar blijft.
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)', // transparant bij dichtheid nul
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)' // felrood bij piekdichtheid
],
},
});
Dit verloop loopt van blauw (dun) via groen en geel naar rood (dicht). Dat sluit aan bij het beeld dat de meeste gebruikers kennen van buienradarkaarten en warmtebeelden. Heeft je app een eigen huisstijlpalet, vervang dan de RGB-waarden door je eigen kleurstops. De interpolatie zorgt automatisch voor vloeiende overgangen.
Designtip: houd voor professionele dashboards en analysetools het deel met lage dichtheid bijna transparant. Zo blijft de basiskaart in rustige gebieden leesbaar en trekken de hotspots vanzelf de aandacht.
Stap 4: voeg een radiusslider toe voor interactieve controle
De radius van de heatmap bepaalt hoe ver elk punt zijn invloed "uitstraalt". Een kleine radius (10 tot 15 px) toont fijnmazige clusters, een grote radius (50 tot 80 px) geeft een breder, vloeiender oppervlak. Verschillende use cases vragen om verschillende standaardwaarden: bezoekersstromen in een druk stadscentrum hebben een kleine radius nodig, landelijke bezorgvraag een grote.
Geef gebruikers een slider, zodat ze direct kunnen bijstellen:
<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>
Koppel de slider aan setPaintProperty, dat de laag bijwerkt zonder hem opnieuw toe te voegen:
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 werkt live en zonder flikkering. De heatmap wordt in hetzelfde frame opnieuw op de GPU gerenderd. Dit patroon werkt voor elke paint-property: dekking, intensiteit en kleurstops.
Stap 5: schaal de intensiteit mee met het zoomniveau
Op lage zoomniveaus (de hele stad in beeld) overlappen nabije punten sterk en kan de heatmap overal verzadigd lijken. Op hoge zoom (straatniveau) liggen dezelfde punten verder uit elkaar en oogt het dichtheidsoppervlak dun. Door de intensiteit aan het zoomniveau te koppelen, blijft de visualisatie op elk zoomniveau leesbaar.
paint: {
// ...overige paint-properties...
'heatmap-intensity': [
'interpolate', ['linear'],
['zoom'],
8, 1, // lage zoom: normale intensiteit
14, 3 // hoge zoom: intensiteit verhogen om de spreiding van punten te compenseren
],
'heatmap-radius': [
'interpolate', ['linear'],
['zoom'],
8, 20, // kleine radius op stadsniveau
14, 50 // grotere radius op straatniveau
],
},
Beide properties gebruiken dezelfde interpolate-expressie over zoom. Waarden tussen de stops worden lineair geïnterpoleerd, dus de overgang verloopt vloeiend terwijl de gebruiker zoomt.
Praktijkvoorbeeld: heatmap van bezorgvraag
Hieronder staat een complete, zelfstandige implementatie voor een dashboard met bezorgvraag. Bestellingen komen uit een API binnen als GeoJSON FeatureCollection. De heatmap wordt bijgewerkt zodra de gebruiker het tijdfilter wijzigt.
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(); // geeft een GeoJSON FeatureCollection terug
}
map.on('load', async () => {
const orders = await loadOrders(8, 12); // ochtendspits
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,
},
});
// Filterknoppen per tijdstip van de dag
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);
});
});
});
De setData-call op een bestaande source vervangt de GeoJSON zonder de laag opnieuw toe te voegen. De heatmap rendert automatisch opnieuw. Dit patroon werkt voor elk tijdfilter: uur van de dag, dag van de week of weersomstandigheden.
Zoek je de routeplanningslaag die meestal bij een heatmap van bezorgvraag hoort, bekijk dan de tutorial Route Optimization API. Leg je een geoptimaliseerde bezorgroute over een heatmap van de vraag, dan hebben dispatchers het complete operationele beeld in één weergave.
Performancetips voor grote datasets
De heatmaplaag van MapAtlas gebruikt WebGL en rendert snel, maar de datapipeline die hem voedt kan op schaal een bottleneck worden.
Aggregeer zeer grote datasets vooraf op de server. Heb je miljoenen GPS-pings, stuur ze dan niet allemaal naar de browser. Voer server-side een ruimtelijke aggregatie uit (H3-hexagonraster, quadtree of eenvoudige afronding op een raster) die je 1 miljoen ruwe punten terugbrengt tot 10.000 rastercellen met een count- en weight-veld. Op gangbare zoomniveaus ziet de heatmap er vrijwel hetzelfde uit, en hij laadt in een fractie van de tijd.
Stream updates incrementeel. Gebruik voor live data (realtime bezoekersstromen, live binnenkomende bestellingen) setData met een rollend venster van recente punten, in plaats van een steeds groter GeoJSON-object op te bouwen. Houd de source op een vast maximum aantal punten en verwijder oude records.
Zet een maxzoom op de source. Met maxzoom: 14 in je addSource-call vraagt de SDK boven zoom 14 geen tiledata meer op. Voor heatmaps maakt dit zelden uit, omdat heatmaplagen één platte GeoJSON-source lezen in plaats van getegelde data, maar het voorkomt onnodige herverwerking op hoge zoomniveaus.
Houd paint-expressies eenvoudig. Het kleurverloop van heatmap-color wordt bij het instellen één keer omgezet in een kleurramp-textuur, dus extra kleurstops kosten per frame vrijwel niets. Op mobiel zijn een grote heatmap-radius en een hoog aantal punten de grotere kostenposten, dus stem die als eerste af.
Initialiseer de kaart lazy. Plaats de volledige kaartinitialisatie in een IntersectionObserver-callback, zodat die pas draait wanneer de kaartcontainer in beeld scrolt. Zo haal je de SDK-bundle uit de eerste paginalading. Dat is vooral waardevol op marketingpagina's waar de kaart onder de vouw staat.
Wil je dieper duiken in performancepatronen voor kaarten, waaronder lazy loading en clustering, dan vind je de volledige lijst in de productiechecklist van Hoe voeg je interactieve kaarten toe aan je website.
Integratie met React
Een heatmap in een React-component volgt hetzelfde patroon als elke MapAtlas-kaart: initialiseer in useEffect, beheer de state voor slider en filter via useState en ruim op bij 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://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]);
// Radius bijwerken zonder de kaart opnieuw te mounten
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>
);
}
Importeer dit component in Next.js met dynamic(() => import('./DeliveryHeatmap'), { ssr: false }). Zo voorkom je server-side renderingfouten door de SDK, die alleen in de browser werkt.
Wat je hierna bouwt
Je hebt nu een werkende interactieve heatmap met gewogen data, een eigen kleurverloop, een radiusslider en zoomgekoppelde intensiteit. Hier kun je verder mee:
- Leg een routinglaag over de heatmap om geplande bezorgroutes af te zetten tegen het vraagoppervlak. De tutorial Route Optimization API behandelt de volledige implementatie.
- Voeg een tijdanimatie toe: loop met een
setInterval-lus door snapshots per uur en roep telkenssetDataaan op de source. Zo wordt een statische dichtheidskaart een timelapse van hoe de vraag zich door de dag verplaatst. - Combineer met vastgoedprijsdata om het prijsverloop per buurt te tonen. De tutorial Real Estate Property Map behandelt patronen voor gewogen data op vastgoedplatforms.
- Bekijk de prijspagina van MapAtlas om het juiste abonnement voor je productieverkeer te vinden.
De volledige SDK-referentie en meer voorbeelden vind je op docs.mapatlas.xyz.
Veelgestelde vragen
Wat is het verschil tussen een heatmap en een choropletenkaart?
Een heatmap visualiseert puntdichtheid door nabije punten te laten samenvloeien tot een doorlopend kleurverloop. Dat is ideaal voor ruwe coördinaten, zoals GPS-pings of locaties van gebeurtenissen. Een choropletenkaart kleurt vooraf gedefinieerde gebieden (landen, postcodegebieden, statistische wijken) op basis van een statistische waarde. Gebruik een heatmap als je veel losse punten hebt, en een choropletenkaart als je data al per regio is geaggregeerd.
Hoeveel datapunten kan een JavaScript-heatmap aan?
De heatmaplaag van MapAtlas rendert via WebGL op de GPU en verwerkt daardoor tienduizenden punten vloeiend op gangbare hardware. Bij veel grotere datasets, vanaf honderdduizenden punten, presteert het beter op eenvoudige apparaten als je de data server-side vooraf aggregeert tot een grover raster en overstapt op een GeoJSON fill-extrusion- of circle-laag.
Kan ik MapAtlas-heatmaps gratis gebruiken?
Ja. MapAtlas heeft een gratis tier met rendering van kaarttegels, ondersteuning voor GeoJSON-lagen en heatmaplagen. Het gratis abonnement dekt ontwikkeling en productiegebruik met een laag volume. Zie mapatlas.eu/pricing voor alle details van de abonnementen.
Werken heatmaps in mobiele browsers?
Ja. De MapAtlas SDK rendert met WebGL, dat alle moderne mobiele browsers ondersteunen, waaronder Safari op iOS en Chrome op Android. Bij zeer grote datasets op eenvoudige mobiele hardware houd je de framerate stabiel door minder punten te tonen of de heatmapradius te vergroten.
Verder lezen:
- Hoe voeg je interactieve kaarten toe aan je website
- Bouw een vastgoedkaart met clustering
- Route-optimalisatie API: verlaag bezorgkosten met 30%
- Begin gratis met bouwen
Veelgestelde vragen
Wat is het verschil tussen een heatmap en een choropletenkaart?
Een heatmap visualiseert puntdichtheid door nabije punten te laten samenvloeien tot een doorlopend kleurverloop. Dat is ideaal voor ruwe coördinaten, zoals GPS-pings of locaties van gebeurtenissen. Een choropletenkaart kleurt vooraf gedefinieerde gebieden (landen, postcodegebieden, statistische wijken) op basis van een statistische waarde. Gebruik een heatmap als je veel losse punten hebt, en een choropletenkaart als je data al per regio is geaggregeerd.
Hoeveel datapunten kan een JavaScript-heatmap aan?
De heatmaplaag van MapAtlas rendert via WebGL op de GPU en verwerkt daardoor tienduizenden punten vloeiend op gangbare hardware. Bij veel grotere datasets, vanaf honderdduizenden punten, presteert het beter op eenvoudige apparaten als je de data server-side vooraf aggregeert tot een grover raster en overstapt op een GeoJSON fill-extrusion- of circle-laag.
Kan ik MapAtlas-heatmaps gratis gebruiken?
Ja. MapAtlas heeft een gratis tier met rendering van kaarttegels, ondersteuning voor GeoJSON-lagen en heatmaplagen. Het gratis abonnement dekt ontwikkeling en productiegebruik met een laag volume. Zie mapatlas.eu/pricing voor alle details van de abonnementen.
Werken heatmaps in mobiele browsers?
Ja. De MapAtlas SDK rendert met WebGL, dat alle moderne mobiele browsers ondersteunen, waaronder Safari op iOS en Chrome op Android. Bij zeer grote datasets op eenvoudige mobiele hardware houd je de framerate stabiel door minder punten te tonen of de heatmapradius te vergroten, waardoor meer punten visueel samensmelten.

