Skip to main content
Mapa de calor JavaScript: tutorial para crear un mapa de densidad
Tutorials

Mapa de calor JavaScript: tutorial para crear un mapa de densidad

Tutorial de mapa de calor en JavaScript: crea un mapa de densidad interactivo con MapAtlas. Formato de datos, ponderación de intensidad y gradientes de color.

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

Un mapa de calor convierte una nube de coordenadas en una superficie de densidad que se entiende de un vistazo. Los colores cálidos marcan dónde se concentran los eventos; los fríos, dónde escasean. Los usuarios captan el patrón antes de leer una sola etiqueta. Por eso los mapas de calor son la opción habitual para analizar el tráfico peatonal, prever la demanda de reparto, crear herramientas de denuncia de delitos o estudiar precios de vivienda.

En este tutorial construyes desde cero un mapa de calor interactivo con el SDK de MapAtlas. Estructurarás los datos de puntos como GeoJSON ponderado, renderizarás una capa de densidad con un degradado de color personalizado, conectarás un control deslizante y terminarás con un ejemplo real de demanda de reparto. Al final tendrás un patrón reutilizable que puedes incorporar a cualquier proyecto de JavaScript o React.

Si es la primera vez que usas el SDK de MapAtlas, lee antes Cómo agregar mapas interactivos a tu sitio web. Cubre la instalación, la inicialización del mapa y los marcadores. Este tutorial continúa donde termina aquel.

Cuándo usar un mapa de calor

Los mapas de calor son la herramienta adecuada cuando tienes un gran conjunto de ubicaciones puntuales y quieres comunicar densidad, no identidad. Los marcadores individuales pierden sentido a partir de unos cientos de puntos; un mapa de calor revela una estructura que ningún número de marcadores podría mostrar.

Casos de uso habituales:

  • Análisis de tráfico peatonal: selección de ubicaciones comerciales, planificación urbana, modelado de aglomeraciones en eventos. Registra un ping GPS por cada cliente que cruza una puerta y el mapa de calor mostrará qué zonas de una ciudad, un centro comercial o un recinto atraen a más gente.
  • Demanda de reparto: agrega el origen de los pedidos por código postal o por coordenada exacta para mostrar a los equipos de despacho dónde se concentra la demanda. Esto alimenta directamente la planificación de zonas y la asignación de conductores.
  • Datos de delitos e incidentes: los paneles de análisis policial, los mapas de riesgo de las aseguradoras y las herramientas de seguridad ciudadana usan mapas de calor de densidad para comunicar el riesgo espacial sin saturar al usuario con marcadores individuales.
  • Gradientes de precios de vivienda: combinado con valores ponderados, un mapa de calor puede mostrar dónde son más altos los precios en una ciudad. Consulta el tutorial de mapa de propiedades inmobiliarias para saber más sobre herramientas de mapas para el sector inmobiliario.

Si tu pregunta es "¿dónde ocurren más cosas?", un mapa de calor es la respuesta.

Formato de datos: puntos GeoJSON ponderados

La capa de mapa de calor de MapAtlas lee una FeatureCollection GeoJSON estándar de features de tipo Point. Cada feature puede llevar una propiedad weight que escala su aportación a la superficie de densidad. Un pedido de 10 artículos aporta más calor que uno de un solo artículo; un gran nudo de transporte genera más tráfico peatonal que una calle secundaria.

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

El valor de weight no tiene unidades. Lo normalizas respecto al rango de tu conjunto de datos. Si tu zona de mayor demanda genera 500 pedidos y la más tranquila 10, conviértelos a una escala de 1 a 10 antes de pasarlos. Así el mapa de calor sigue siendo significativo con conjuntos de datos cuyas cifras absolutas son muy distintas.

Sin propiedad weight, todos los puntos aportan lo mismo y el mapa de calor refleja la densidad por simple recuento.

Requisitos previos

Antes de empezar necesitas:

  • Una clave de API de MapAtlas (regístrate gratis, sin tarjeta de crédito)
  • Node.js 18+ para proyectos con npm, o una página HTML sencilla si prefieres usar la CDN

Paso 1: instala e inicializa el mapa

Instala el SDK:

npm install @mapmetrics/mapmetrics-gl

O cárgalo mediante CDN en un archivo HTML sencillo:

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

Añade un contenedor con una altura definida:

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

Inicializa el mapa con un estilo oscuro de tu cuenta. Una base oscura hace que los degradados de color del mapa de calor destaquen sobre el fondo, y por eso es la base preferida para visualizaciones de densidad:

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], // Ámsterdam
  zoom: 12,
});

Paso 2: añade la capa de mapa de calor

Registra tu GeoJSON como fuente y añade una capa de mapa de calor que lea de ella. El callback map.on('load', ...) garantiza que el estilo ha terminado de cargarse antes de modificarlo.

map.on('load', () => {
  map.addSource('demand', {
    type: 'geojson',
    data: demandData,
  });

  map.addLayer({
    id: 'demand-heatmap',
    type: 'heatmap',
    source: 'demand',
    paint: {
      // Pondera cada punto según su propiedad 'weight' (1 por defecto si no existe)
      'heatmap-weight': [
        'interpolate', ['linear'],
        ['get', 'weight'],
        0, 0,
        12, 1
      ],
      // Radio en píxeles; más grande = más suave pero menos preciso
      'heatmap-radius': 30,
      // Opacidad general de la capa de mapa de calor
      'heatmap-opacity': 0.85,
    },
  });
});

En este punto ya tienes un mapa de calor funcionando. Las zonas azul verdosas tienen poca densidad; el degradado por defecto pasa a amarillo y rojo en las zonas de alta densidad. El siguiente paso sustituye la paleta por defecto por un esquema de color elegido a propósito.

Paso 3: personaliza el degradado de color

La propiedad heatmap-color asigna colores a los valores de densidad (de 0 a 1) con la misma expresión interpolate que se usa en el resto del sistema de pintura de MapAtlas. La densidad 0 es transparente, para que la capa base del mapa se vea en las zonas vacías.

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 con densidad cero
      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)'   // rojo intenso en la densidad máxima
    ],
  },
});

Este degradado va del azul (disperso) al rojo (denso), pasando por verde y amarillo, y encaja con el modelo mental que la mayoría de los usuarios trae de los mapas de radar meteorológico y las imágenes térmicas. Si tu app tiene una paleta de colores de marca, sustituye los valores RGB por tus propias paradas; la interpolación se encarga de suavizar las transiciones.

Consejo de diseño: en paneles profesionales y herramientas de analítica, mantén casi transparente el extremo de baja densidad. Así conservas la legibilidad del mapa base en las zonas tranquilas y la vista se dirige de forma natural a las zonas calientes.

Paso 4: añade un control deslizante de radio

El radio del mapa de calor controla hasta dónde "irradia" su influencia cada punto. Un radio pequeño (de 10 a 15 px) muestra agrupaciones de detalle fino; uno grande (de 50 a 80 px) produce una superficie más amplia y suave. Cada caso de uso pide un valor por defecto distinto: el tráfico peatonal en un centro urbano denso necesita un radio pequeño, y la demanda de reparto de todo un país, uno grande.

Dale al usuario un control deslizante para ajustarlo sobre la marcha:

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

Conecta el control a setPaintProperty, que actualiza la capa sin tener que volver a añadirla:

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 actualiza en directo y sin parpadeos. El mapa de calor se vuelve a renderizar en la GPU en el mismo fotograma. Este patrón sirve para cualquier propiedad de pintura: opacidad, intensidad, paradas de color.

Paso 5: escala la intensidad según el nivel de zoom

Con zoom bajo (vista de toda la ciudad), los puntos cercanos se solapan mucho y el mapa de calor puede parecer saturado de manera uniforme. Con zoom alto (nivel de calle), esos mismos puntos se separan y la superficie de densidad parece escasa. Compensar la intensidad en función del zoom mantiene la visualización legible a cualquier escala.

paint: {
  // ...resto de propiedades de pintura...
  'heatmap-intensity': [
    'interpolate', ['linear'],
    ['zoom'],
    8,  1,   // zoom bajo: intensidad normal
    14, 3    // zoom alto: aumenta la intensidad para compensar la dispersión de los puntos
  ],
  'heatmap-radius': [
    'interpolate', ['linear'],
    ['zoom'],
    8,  20,  // radio pequeño a escala de ciudad
    14, 50   // radio mayor a escala de calle
  ],
},

Ambas propiedades usan la misma expresión interpolate sobre zoom. Los valores entre paradas se interpolan de forma lineal, así que la transición es suave mientras el usuario hace zoom.

Ejemplo real: mapa de calor de demanda de reparto

Esta es una implementación completa y autónoma para un panel de demanda de reparto. Los pedidos llegan desde una API como FeatureCollection GeoJSON. El mapa de calor se actualiza cada vez que el usuario cambia el filtro horario.

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(); // devuelve una FeatureCollection GeoJSON
}

map.on('load', async () => {
  const orders = await loadOrders(8, 12); // pico de la mañana

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

  // Botones de filtro por franja horaria
  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);
    });
  });
});

La llamada setData sobre una fuente existente sustituye el GeoJSON sin volver a añadir la capa. El mapa de calor se vuelve a renderizar automáticamente. Este patrón sirve para cualquier filtro temporal: hora del día, día de la semana, condiciones meteorológicas.

Para la capa de planificación de rutas que suele acompañar a un mapa de calor de demanda de reparto, consulta el guía de optimización de rutas. Superponer una ruta de reparto optimizada sobre un mapa de calor de demanda da a los equipos de despacho una imagen operativa completa en una sola vista.

Consejos de rendimiento para grandes conjuntos de datos

La capa de mapa de calor de MapAtlas usa WebGL y renderiza con rapidez, pero la cadena de datos que la alimenta puede convertirse en un cuello de botella a gran escala.

Preagrega en el servidor los conjuntos de datos muy grandes. Si tienes millones de pings GPS, no los envíes todos al navegador. Haz una agregación espacial en el servidor (rejilla hexagonal H3, quadtree o un simple redondeo a una cuadrícula) que reduzca tu millón de puntos en bruto a 10.000 celdas con un campo count y otro weight. En los niveles de zoom habituales, el mapa de calor se verá casi igual y cargará en una fracción del tiempo.

Envía las actualizaciones de forma incremental. Con datos en directo (tráfico peatonal en tiempo real, pedidos que entran al momento), usa setData con una ventana deslizante de puntos recientes en lugar de acumular un objeto GeoJSON que no para de crecer. Mantén la fuente con un número máximo fijo de puntos y descarta los registros antiguos.

Usa maxzoom en la fuente. Añadir maxzoom: 14 a tu llamada addSource indica al SDK que deje de pedir datos de teselas por encima del zoom 14. En los mapas de calor rara vez importa, porque estas capas leen una única fuente GeoJSON plana en lugar de datos en teselas, pero evita reprocesamientos innecesarios en los niveles de zoom altos.

Mantén simples las expresiones de pintura. El degradado de heatmap-color se convierte una sola vez en una textura de rampa de color cuando lo defines, así que las paradas de color adicionales casi no cuestan nada por fotograma. En móviles, un heatmap-radius grande y un número alto de puntos son los costes más importantes, así que ajusta esos primero.

Inicializa el mapa de forma diferida. Envuelve toda la inicialización del mapa en un callback de IntersectionObserver para que solo se ejecute cuando el contenedor del mapa entre en pantalla. Así retrasas la carga del bundle del SDK respecto a la carga inicial de la página, algo especialmente útil en páginas de marketing donde el mapa queda por debajo de la primera pantalla.

Para profundizar en patrones de rendimiento de mapas, como la carga diferida y la agrupación, la lista de comprobación para producción de Cómo agregar mapas interactivos a tu sitio web recoge la lista completa.

Integración con React

Envolver el mapa de calor en un componente de React sigue el mismo patrón que cualquier mapa de MapAtlas: inicializa en useEffect, expón el estado del control deslizante y del filtro con useState y limpia al 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]);

  // Actualiza el radio sin volver a montar el 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>
  );
}

En Next.js, importa este componente con dynamic(() => import('./DeliveryHeatmap'), { ssr: false }) para evitar errores de renderizado en el servidor, ya que el SDK solo funciona en el navegador.

Qué construir a continuación

Ya tienes un mapa de calor interactivo con datos ponderados, un degradado de color personalizado, un control deslizante de radio e intensidad ligada al zoom. Estos son los siguientes pasos:

  • Superpón una capa de rutas sobre el mapa de calor para mostrar las rutas de reparto planificadas frente a la superficie de demanda. El guía de optimización de rutas explica cómo funciona el solucionador, qué datos de entrada necesita y qué restricciones admite.
  • Añade una animación temporal: recorre instantáneas por horas con un bucle setInterval que llame a setData sobre la fuente. Así conviertes un mapa de densidad estático en un time-lapse de cómo se desplaza la demanda a lo largo del día.
  • Combínalo con datos de precios de vivienda para mostrar gradientes de precio por barrio. El tutorial de mapa de propiedades inmobiliarias cubre patrones de datos ponderados para plataformas inmobiliarias.
  • Consulta la página de precios de MapAtlas para elegir el plan adecuado a tu tráfico en producción.

La referencia completa del SDK y más ejemplos están disponibles en docs.mapatlas.xyz.

Preguntas frecuentes

¿Qué diferencia hay entre un mapa de calor y un mapa coroplético?

Un mapa de calor visualiza la densidad de puntos fundiendo los puntos cercanos en un degradado de color continuo, ideal para coordenadas en bruto como pings GPS o ubicaciones de eventos. Un mapa coroplético colorea áreas geográficas predefinidas (países, códigos postales, secciones censales) según un valor estadístico. Usa un mapa de calor cuando tengas muchos puntos individuales; usa un coroplético cuando tus datos ya estén agregados por región.

¿Cuántos puntos de datos admite un mapa de calor en JavaScript?

La capa de mapa de calor de MapAtlas se renderiza en la GPU mediante WebGL, así que maneja decenas de miles de puntos con fluidez en hardware típico. Con conjuntos de datos mucho mayores, a partir de cientos de miles de puntos, preagregar los datos en el servidor en una cuadrícula de menor resolución y pasar a una capa GeoJSON de tipo fill-extrusion o circle da mejor rendimiento en dispositivos modestos.

¿Puedo usar los mapas de calor de MapAtlas gratis?

Sí. MapAtlas tiene un plan gratuito que incluye el renderizado de teselas, las capas GeoJSON y las capas de mapa de calor. El plan gratuito cubre el desarrollo y el uso en producción con poco volumen. Consulta mapatlas.eu/pricing para ver todos los detalles de los planes.

¿Funcionan los mapas de calor en navegadores móviles?

Sí. El SDK de MapAtlas usa WebGL para renderizar, compatible con todos los navegadores móviles modernos, incluidos Safari en iOS y Chrome en Android. Con conjuntos de datos muy grandes en móviles de gama baja, reducir el número de puntos o aumentar el radio del mapa de calor mantiene la fluidez.


Lecturas relacionadas:

Preguntas frecuentes

¿Qué diferencia hay entre un mapa de calor y un mapa coroplético?

Un mapa de calor visualiza la densidad de puntos fundiendo los puntos cercanos en un degradado de color continuo, ideal para coordenadas en bruto como pings GPS o ubicaciones de eventos. Un mapa coroplético colorea áreas geográficas predefinidas (países, códigos postales, secciones censales) según un valor estadístico. Usa un mapa de calor cuando tengas muchos puntos individuales; usa un coroplético cuando tus datos ya estén agregados por región.

¿Cuántos puntos de datos admite un mapa de calor en JavaScript?

La capa de mapa de calor de MapAtlas se renderiza en la GPU mediante WebGL, así que maneja decenas de miles de puntos con fluidez en hardware típico. Con conjuntos de datos mucho mayores, a partir de cientos de miles de puntos, preagregar los datos en el servidor en una cuadrícula de menor resolución y pasar a una capa GeoJSON de tipo fill-extrusion o circle da mejor rendimiento en dispositivos modestos.

¿Puedo usar los mapas de calor de MapAtlas gratis?

Sí. MapAtlas tiene un plan gratuito que incluye el renderizado de teselas, las capas GeoJSON y las capas de mapa de calor. El plan gratuito cubre el desarrollo y el uso en producción con poco volumen. Consulta mapatlas.eu/pricing para ver todos los detalles de los planes.

¿Funcionan los mapas de calor en navegadores móviles?

Sí. El SDK de MapAtlas usa WebGL para renderizar, compatible con todos los navegadores móviles modernos, incluidos Safari en iOS y Chrome en Android. Con conjuntos de datos muy grandes en móviles de gama baja, reducir el número de puntos o aumentar el radio del mapa de calor (que funde más puntos visualmente) mantiene la fluidez.

¿Le resultó útil? Compártalo.

Sobre el autor

Brent van der Heiden

Escrito por

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. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

Ver todos los artículos →
Volver al blog