Um heatmap (mapa de calor) transforma uma nuvem de coordenadas em uma superfície de densidade que se lê em um instante. Cores quentes marcam onde os eventos se concentram; cores frias marcam onde eles rareiam. O usuário entende o padrão antes de ler qualquer rótulo. Por isso os heatmaps são a escolha padrão para análise de fluxo de pessoas, previsão de demanda de entregas, ferramentas de registro de ocorrências criminais e pesquisa de preços de imóveis.
Este tutorial constrói um heatmap interativo do zero com o SDK do MapAtlas. Você vai estruturar os dados de pontos como GeoJSON ponderado, renderizar uma camada de densidade com um gradiente de cores personalizado, conectar um controle deslizante de intensidade e terminar com um exemplo real de demanda de entregas. No final, você terá um padrão reutilizável para aplicar em qualquer projeto JavaScript ou React.
Se você ainda não conhece o SDK do MapAtlas, leia primeiro Como adicionar mapas interativos ao seu site. Ele cobre instalação, inicialização do mapa e marcadores. Este tutorial continua de onde aquele parou.
Quando usar um heatmap
O heatmap é a ferramenta certa quando você tem um grande conjunto de pontos individuais e quer comunicar densidade, não identidade. Marcadores individuais perdem sentido depois de algumas centenas de pontos; um heatmap revela uma estrutura que nenhuma quantidade de pinos conseguiria mostrar.
Casos de uso comuns:
- Análise de fluxo de pessoas: escolha de pontos de varejo, planejamento urbano, modelagem de público em eventos. Registre um ping de GPS para cada cliente que passou por uma porta e o heatmap mostra quais áreas de uma cidade, shopping ou local de evento atraem mais gente.
- Demanda de entregas: agregue as origens dos pedidos por CEP ou por coordenada bruta para mostrar aos despachantes onde a demanda se concentra. Isso alimenta diretamente o planejamento de zonas e a alocação de entregadores.
- Dados de crimes e incidentes: painéis de análise policial, mapas de risco de seguradoras e ferramentas de segurança pública usam heatmaps de densidade para comunicar risco espacial sem sobrecarregar o usuário com marcadores individuais.
- Gradientes de preços de imóveis: combinado com valores ponderados, um heatmap mostra onde os preços são mais altos em uma cidade. Veja o tutorial de mapa de imóveis para saber mais sobre ferramentas de mapa voltadas ao mercado imobiliário.
Se a sua pergunta é "onde as coisas acontecem com mais frequência?", um heatmap é a resposta.
Formato dos dados: pontos GeoJSON ponderados
A camada de heatmap do MapAtlas lê uma FeatureCollection GeoJSON padrão com features do tipo Point. Cada feature pode ter uma propriedade weight que ajusta sua contribuição para a superfície de densidade. Um pedido de 10 itens gera mais calor que um pedido de um item só; um grande terminal de transporte gera mais fluxo de pessoas que uma rua secundária.
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" }
}
]
};
O valor de weight é adimensional. Você o normaliza em relação à faixa de valores do seu dataset. Se a zona com mais demanda gera 500 pedidos e a mais tranquila gera 10, converta esses números para uma escala de 1 a 10 antes de passá-los ao mapa. Assim o heatmap continua visualmente útil em datasets com contagens absolutas muito diferentes.
Sem a propriedade weight, todos os pontos contribuem igualmente e o heatmap reflete apenas a densidade por contagem.
Pré-requisitos
Antes de começar, você precisa de:
- Uma chave de API do MapAtlas (cadastre-se grátis, sem cartão de crédito)
- Node.js 18+ para projetos baseados em npm, ou uma página HTML simples se preferir usar a CDN
Passo 1: instale e inicialize o mapa
Instale o SDK:
npm install @mapmetrics/mapmetrics-gl
Ou carregue via CDN em um arquivo HTML simples:
<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>
Adicione um container com altura definida:
<div id="map" style="width: 100%; height: 600px;"></div>
Inicialize o mapa com um estilo escuro da sua conta. Uma base escura faz os gradientes do heatmap se destacarem sobre o fundo, e por isso é a base preferida para visualizações de densidade:
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,
});
Passo 2: adicione a camada de heatmap
Registre seu GeoJSON como source e depois adicione uma camada de heatmap que lê dessa source. O callback map.on('load', ...) garante que o estilo terminou de carregar antes de você modificá-lo.
map.on('load', () => {
map.addSource('demand', {
type: 'geojson',
data: demandData,
});
map.addLayer({
id: 'demand-heatmap',
type: 'heatmap',
source: 'demand',
paint: {
// Pondera cada ponto pela propriedade 'weight' (1 por padrão, se ausente)
'heatmap-weight': [
'interpolate', ['linear'],
['get', 'weight'],
0, 0,
12, 1
],
// Raio em pixels; maior = mais suave, porém menos preciso
'heatmap-radius': 30,
// Opacidade geral da camada de heatmap
'heatmap-opacity': 0.85,
},
});
});
Neste ponto você já tem um heatmap funcionando. Áreas azul-esverdeadas indicam baixa densidade; o gradiente padrão passa para amarelo e vermelho nas áreas de alta densidade. O próximo passo troca a paleta padrão por um esquema de cores escolhido por você.
Passo 3: personalize o gradiente de cores
A propriedade heatmap-color associa valores de densidade (0 a 1) a cores usando a mesma expressão interpolate usada no restante do sistema de paint do MapAtlas. A densidade 0 é transparente, para que a camada base do mapa apareça nas áreas vazias.
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)', // transparente com densidade zero
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)' // vermelho intenso no pico de densidade
],
},
});
Esse gradiente vai do azul (esparso), passando por verde e amarelo, até o vermelho (denso). É o mesmo modelo mental que a maioria dos usuários traz de radares meteorológicos e imagens térmicas. Se o seu app tem uma paleta de cores da marca, substitua os valores RGB pelos seus próprios pontos de parada; a interpolação cuida das transições suaves automaticamente.
Dica de design: em dashboards profissionais e ferramentas de análise, mantenha a faixa de baixa densidade quase transparente. Isso preserva a legibilidade do mapa base nas áreas tranquilas e leva o olhar naturalmente às zonas quentes.
Passo 4: adicione um controle deslizante de raio
O raio do heatmap controla até onde cada ponto "irradia" influência. Um raio pequeno (10 a 15px) mostra agrupamentos detalhados; um raio grande (50 a 80px) produz uma superfície mais ampla e suave. Casos de uso diferentes pedem valores padrão diferentes: o fluxo de pessoas em um centro urbano denso precisa de um raio pequeno, e a demanda de entregas em um país inteiro precisa de um raio grande.
Dê ao usuário um controle deslizante para ajustar em tempo real:
<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>
Conecte o controle deslizante a setPaintProperty, que atualiza a camada sem precisar adicioná-la de novo:
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 faz uma atualização ao vivo, sem piscar. O heatmap é renderizado de novo na GPU no mesmo frame. Esse padrão funciona para qualquer propriedade de paint: opacidade, intensidade, paradas de cor.
Passo 5: ajuste a intensidade pelo nível de zoom
Em níveis de zoom baixos (visão da cidade inteira), pontos próximos se sobrepõem muito e o heatmap pode parecer uniformemente saturado. Em zoom alto (nível da rua), os mesmos pontos se espalham e a superfície de densidade parece rala. Compensar a intensidade de acordo com o zoom mantém a visualização legível em qualquer escala.
paint: {
// ...outras propriedades de paint...
'heatmap-intensity': [
'interpolate', ['linear'],
['zoom'],
8, 1, // zoom baixo: intensidade normal
14, 3 // zoom alto: aumenta a intensidade para compensar a dispersão dos pontos
],
'heatmap-radius': [
'interpolate', ['linear'],
['zoom'],
8, 20, // raio pequeno na escala da cidade
14, 50 // raio maior na escala da rua
],
},
As duas propriedades usam a mesma expressão interpolate sobre zoom. Os valores entre as paradas são interpolados linearmente, então a transição é suave enquanto o usuário aplica zoom.
Exemplo real: heatmap de demanda de entregas
A seguir, uma implementação completa e independente para um dashboard de demanda de entregas. Os pedidos chegam de uma API como uma FeatureCollection GeoJSON. O heatmap é atualizado sempre que o usuário muda o filtro de horário.
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(); // retorna uma FeatureCollection GeoJSON
}
map.on('load', async () => {
const orders = await loadOrders(8, 12); // pico da manhã
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,
},
});
// Botões de filtro por horário do dia
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);
});
});
});
A chamada setData em uma source existente substitui o GeoJSON sem adicionar a camada de novo. O heatmap é renderizado novamente de forma automática. Esse padrão serve para qualquer filtro temporal: hora do dia, dia da semana, condição do tempo.
Para a camada de planejamento de rotas que costuma acompanhar um heatmap de demanda de entregas, veja o tutorial da API de otimização de rotas. Sobrepor uma rota de entrega otimizada a um heatmap de demanda dá aos despachantes um panorama operacional completo em uma única tela.
Dicas de desempenho para grandes datasets
A camada de heatmap do MapAtlas usa WebGL e renderiza rápido, mas o pipeline de dados que a alimenta pode virar gargalo em grande escala.
Pré-agregue no servidor quando o dataset for muito grande. Se você tem milhões de pings de GPS, não envie todos para o navegador. Faça uma agregação espacial no servidor (grade hexagonal H3, quadtree ou um simples arredondamento em grade) que reduza 1 milhão de pontos brutos a 10.000 células com campos count e weight. Nos níveis de zoom mais comuns, o heatmap fica praticamente idêntico e carrega em uma fração do tempo.
Envie atualizações de forma incremental. Para dados ao vivo (fluxo de pessoas em tempo real, pedidos sendo feitos), use setData com uma janela deslizante de pontos recentes em vez de acumular um objeto GeoJSON que só cresce. Mantenha a source com um número máximo fixo de pontos e descarte os registros antigos.
Use maxzoom na source. Adicionar maxzoom: 14 à chamada addSource faz o SDK parar de solicitar dados de tiles acima do zoom 14. Para heatmaps isso raramente importa, já que as camadas de heatmap leem uma única source GeoJSON plana em vez de dados em tiles, mas evita reprocessamento desnecessário em zooms altos.
Mantenha as expressões de paint simples. O gradiente de heatmap-color é convertido uma única vez em uma textura de rampa de cores quando você o define, então paradas de cor extras quase não custam nada por frame. No celular, um heatmap-radius grande e uma quantidade alta de pontos pesam mais, então ajuste esses dois primeiro.
Inicialize o mapa sob demanda (lazy). Coloque toda a inicialização do mapa dentro de um callback de IntersectionObserver, para que ela só rode quando o container do mapa entrar na área visível. Isso tira o bundle do SDK do carregamento inicial da página e ajuda especialmente em páginas de marketing onde o mapa fica abaixo da dobra.
Para se aprofundar em padrões de desempenho de mapas, incluindo lazy loading e clustering, o checklist de produção em Como adicionar mapas interativos ao seu site traz a lista completa.
Integração com React
Encapsular o heatmap em um componente React segue o mesmo padrão de qualquer mapa do MapAtlas: inicialize em useEffect, exponha o estado do controle deslizante e do filtro com useState e faça a limpeza ao desmontar.
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]);
// Atualiza o raio sem remontar o mapa
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>
);
}
No Next.js, importe esse componente com dynamic(() => import('./DeliveryHeatmap'), { ssr: false }) para evitar erros de renderização no servidor causados pelo SDK, que só roda no navegador.
O que construir a seguir
Agora você tem um heatmap interativo funcionando, com dados ponderados, gradiente de cores personalizado, controle deslizante de raio e intensidade ajustada pelo zoom. Veja como evoluir:
- Sobreponha uma camada de rotas ao heatmap para mostrar as rotas de entrega planejadas sobre a superfície de demanda. O tutorial da API de otimização de rotas mostra a implementação completa.
- Adicione uma animação temporal: percorra snapshots por hora com um loop de
setIntervalque chamasetDatana source. Isso transforma um mapa de densidade estático em um time-lapse de como a demanda se desloca ao longo do dia. - Combine com dados de preços de imóveis para mostrar gradientes de preço por bairro. O tutorial de mapa de imóveis cobre padrões de dados ponderados para plataformas imobiliárias.
- Consulte a página de preços do MapAtlas para escolher o plano certo para o seu tráfego em produção.
A referência completa do SDK e outros exemplos estão em docs.mapatlas.xyz.
Perguntas frequentes
Qual é a diferença entre um heatmap e um mapa coroplético?
Um heatmap visualiza a densidade de pontos misturando pontos próximos em um gradiente de cor contínuo, ideal para coordenadas brutas como pings de GPS ou locais de eventos. Um mapa coroplético colore áreas geográficas predefinidas (países, CEPs, setores censitários) de acordo com um valor estatístico. Use um heatmap quando tiver muitos pontos individuais; use um mapa coroplético quando seus dados já estiverem agregados por região.
Quantos pontos um heatmap em JavaScript aguenta?
A camada de heatmap do MapAtlas renderiza na GPU via WebGL, então lida com dezenas de milhares de pontos com fluidez em hardware comum. Para datasets bem maiores, a partir de centenas de milhares de pontos, pré-agregar os dados no servidor em uma grade de menor resolução e trocar para uma camada GeoJSON de fill-extrusion ou de círculos oferece melhor desempenho em dispositivos mais simples.
Posso usar os heatmaps do MapAtlas de graça?
Sim. O MapAtlas tem um plano gratuito que inclui renderização de tiles de mapa, suporte a camadas GeoJSON e camadas de heatmap. O plano gratuito cobre desenvolvimento e uso em produção com baixo volume. Veja mapatlas.eu/pricing para os detalhes de cada plano.
Heatmaps funcionam em navegadores de celular?
Sim. O SDK do MapAtlas usa WebGL para renderizar, e o WebGL é suportado em todos os navegadores móveis modernos, incluindo o Safari no iOS e o Chrome no Android. Para datasets muito grandes em celulares mais simples, reduzir o número de pontos ou aumentar o raio do heatmap mantém a taxa de frames estável.
Leitura relacionada:
- Como adicionar mapas interativos ao seu site
- Crie um mapa de imóveis com clustering
- API de otimização de rotas: reduza custos de entrega em 30%
- Comece a construir de graça
Perguntas frequentes
Qual é a diferença entre um heatmap e um mapa coroplético?
Um heatmap visualiza a densidade de pontos misturando pontos próximos em um gradiente de cor contínuo, ideal para coordenadas brutas como pings de GPS ou locais de eventos. Um mapa coroplético colore áreas geográficas predefinidas (países, CEPs, setores censitários) de acordo com um valor estatístico. Use um heatmap quando tiver muitos pontos individuais; use um mapa coroplético quando seus dados já estiverem agregados por região.
Quantos pontos um heatmap em JavaScript aguenta?
A camada de heatmap do MapAtlas renderiza na GPU via WebGL, então lida com dezenas de milhares de pontos com fluidez em hardware comum. Para datasets bem maiores, a partir de centenas de milhares de pontos, pré-agregar os dados no servidor em uma grade de menor resolução e trocar para uma camada GeoJSON de fill-extrusion ou de círculos oferece melhor desempenho em dispositivos mais simples.
Posso usar os heatmaps do MapAtlas de graça?
Sim. O MapAtlas tem um plano gratuito que inclui renderização de tiles de mapa, suporte a camadas GeoJSON e camadas de heatmap. O plano gratuito cobre desenvolvimento e uso em produção com baixo volume. Veja mapatlas.eu/pricing para os detalhes de cada plano.
Heatmaps funcionam em navegadores de celular?
Sim. O SDK do MapAtlas usa WebGL para renderizar, e o WebGL é suportado em todos os navegadores móveis modernos, incluindo o Safari no iOS e o Chrome no Android. Para datasets muito grandes em celulares mais simples, reduzir o número de pontos ou aumentar o raio do heatmap (o que mescla visualmente mais pontos) mantém a taxa de frames estável.

