Clustering inteligente más allá de MarkerCluster

  • Categoría de la entrada:Discord Exclusivo
  • Tiempo de lectura:10 minutos de lectura

Si llevas tiempo trabajando con mapas web, es posible que hayas usado Leaflet.MarkerCluster. Funciona bien para casos básicos, pero cuando tienes muchos puntos o necesitas más control sobre el clustering, las cosas se complican.

Hoy te muestro Supercluster.js, la librería que Mapbox usa internamente para hacer clustering. Es más rápida, más flexible y te da control total sobre cómo se agrupan tus marcadores.

¿Por qué deberías usar Supercluster?

Leaflet.MarkerCluster es un plugin “todo en uno” ya que hace el clustering y renderiza los marcadores. Pero sus contras se evidencian cuando:

  • Tienes más de 10,000 puntos y el performance se degrada.
  • Quieres personalizar completamente el aspecto visual.
  • Necesitas clustering en otras librerías además de Leaflet.
  • Quieres más control sobre los algoritmos de agrupación.

Supercluster.js solo hace una cosa: calcular clusters. Tú decides cómo renderizarlos. Esto lo hace:

  • Sea más rápido porque procesa 100,000 puntos en milisegundos.
  • Funciona con Leaflet, Mapbox, OpenLayers, o canvas puro, lo que lo hace mas flexible.
  • Es más ligero ya que 6KB vs 50KB de MarkerCluster hace la diferencia.
  • Ajustas radio, zoom min/max y propiedades agregadas.

Instalación

En terminal de comandos

O desde CDN:

Tu archivo html

<script src="https://unpkg.com/supercluster@8.0.1/dist/supercluster.min.js"></script>

Implementación básica con Leaflet

Aquí está el código completo para integrar Supercluster con Leaflet:

Tu archivo javascript

// 1. Inicializar el mapa
const map = L.map('map').setView([4.6097, -74.0817], 10);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '© OpenStreetMap contributors'
}).addTo(map);

// 2. Preparar tus datos en formato GeoJSON
const puntos = [
  {
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [-74.0817, 4.6097] // [lng, lat]
    },
    properties: {
      nombre: 'Punto 1',
      categoria: 'restaurante'
    }
  },
  {
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [-74.0520, 4.6789]
    },
    properties: {
      nombre: 'Punto 2',
      categoria: 'hotel'
    }
  }
  // ... aqui colocas todos los demás puntos
];

// 3. Configurar Supercluster
const index = new Supercluster({
  radius: 60,        // Radio de clustering en pixels
  maxZoom: 16,       // Zoom máximo donde se hace clustering
  minZoom: 0,        // Zoom mínimo
  minPoints: 2       // Mínimo de puntos para formar un cluster
});

// 4. Cargar los datos
index.load(puntos);

// 5. Capa de marcadores
let markers = L.layerGroup().addTo(map);

// 6. Función para actualizar clusters
function actualizarClusters() {
  // Limpiar marcadores anteriores
  markers.clearLayers();
  
  // Obtener bounds y zoom actual
  const bounds = map.getBounds();
  const bbox = [
    bounds.getWest(),
    bounds.getSouth(),
    bounds.getEast(),
    bounds.getNorth()
  ];
  const zoom = map.getZoom();
  
  // Obtener clusters para el área visible
  const clusters = index.getClusters(bbox, Math.floor(zoom));
  
  // Renderizar cada cluster o punto
  clusters.forEach(cluster => {
    const [lng, lat] = cluster.geometry.coordinates;
    
    if (cluster.properties.cluster) {
      // Es un cluster
      const count = cluster.properties.point_count;
      const marker = L.marker([lat, lng], {
        icon: L.divIcon({
          html: `<div class="cluster-marker">${count}</div>`,
          className: 'cluster-icon',
          iconSize: [40, 40]
        })
      });
      
      // Click para hacer zoom
      marker.on('click', () => {
        const expansionZoom = index.getClusterExpansionZoom(
          cluster.properties.cluster_id
        );
        map.setView([lat, lng], expansionZoom);
      });
      
      markers.addLayer(marker);
      
    } else {
      // Es un punto individual
      const marker = L.circleMarker([lat, lng], {
        radius: 8,
        fillColor: '#3388ff',
        color: '#fff',
        weight: 2,
        fillOpacity: 0.8
      });
      
      marker.bindPopup(`
        <strong>${cluster.properties.nombre}</strong><br>
        ${cluster.properties.categoria}
      `);
      
      markers.addLayer(marker);
    }
  });
}

// 7. Actualizar cuando cambia el mapa
map.on('moveend', actualizarClusters);
map.on('zoomend', actualizarClusters);

// Renderizar inicial
actualizarClusters();

Estilos CSS para los clusters

Tu archivo css

.cluster-marker {
  background: #3388ff;
  color: white;
  border: 3px solid white;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  font-size: 14px;
  box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}

.cluster-icon {
  background: transparent !important;
  border: none !important;
}

Opciones avanzadas de configuración

Tu archivo javascript

const index = new Supercluster({
  radius: 60,           // Radio de agrupación (default: 40)
  maxZoom: 16,          // Zoom máximo de clustering (default: 16)
  minZoom: 0,           // Zoom mínimo (default: 0)
  minPoints: 2,         // Puntos mínimos para cluster (default: 2)
  
  // Función para generar propiedades agregadas
  reduce: (accumulated, props) => {
    // Sumar una propiedad personalizada
    accumulated.sum += props.valor;
  },
  
  // Propiedades iniciales del cluster
  initial: () => ({ sum: 0 }),
  
  // Mapear propiedades de cada punto
  map: props => ({ valor: props.precio || 0 })
});

Ejemplo: Agregación de datos en clusters

Supongamos que tienes propiedades inmobiliarias y quieres mostrar el precio promedio en cada cluster:

Tu archivo javascript

const index = new Supercluster({
  radius: 80,
  maxZoom: 16,
  
  // Calcular suma de precios
  reduce: (accumulated, props) => {
    accumulated.sumaPrecio += props.precio;
  },
  
  // Estado inicial del cluster
  initial: () => ({ sumaPrecio: 0 }),
  
  // Extraer precio de cada punto
  map: props => ({ precio: props.precio || 0 })
});

// Al renderizar, calcular promedio
clusters.forEach(cluster => {
  if (cluster.properties.cluster) {
    const count = cluster.properties.point_count;
    const suma = cluster.properties.sumaPrecio;
    const promedio = Math.round(suma / count);
    
    const marker = L.marker([lat, lng], {
      icon: L.divIcon({
        html: `
          <div class="cluster-marker">
            <div class="count">${count}</div>
            <div class="price">$${promedio.toLocaleString()}</div>
          </div>
        `,
        className: 'cluster-icon',
        iconSize: [60, 60]
      })
    });
  }
});

Clustering vs Heatmaps. ¿Cuándo usar cada uno?

Usa Clustering cuando:

  • Necesitas mostrar ubicaciones exactas de puntos individuales.
  • El usuario necesita interactuar con cada marcador como por ejemplo con click o un popup.
  • Tienes datos con categorías.
  • Quieres que el usuario pueda hacer zoom para ver más detalle.
  • Los puntos tienen información específica que debe mostrarse.

Ejemplos: Locales comerciales, eventos, reportes ciudadanos, puntos de interés turístico.

Usa Heatmaps cuando:

  • Quieres mostrar densidad o intensidad general.
  • Las ubicaciones exactas no son lo más importante.
  • Tienes datos numéricos continuos como temperatura o criminalidad.
  • Quieres visualizar patrones espaciales o “hot spots”.
  • La cantidad de datos es tan grande que los marcadores serían ilegibles.

Ejemplos: Mapa de calor de densidad de población, zonas de mayor tráfico, temperatura ambiente.

Combina ambos cuando:

  • Heatmap de fondo mostrando densidad general.
  • Clusters visibles al hacer zoom para ver ubicaciones específicas.
  • Ejemplo: Mapa de accidentes de tránsito (heatmap de zonas peligrosas + clusters de accidentes individuales al acercar).

Tips finales

  1. Formato de coordenadas: Supercluster usa [lng, lat], no [lat, lng] como Leaflet.
  2. Reindexar datos: Si tus datos cambian, llama a index.load(nuevosDatos) de nuevo.
  3. getLeaves(): Usa esta función para obtener todos los puntos dentro de un cluster.
  4. getTile(): Útil si quieres generar tiles de clustering del lado del servidor.

Tu archivo javascript

// Obtener puntos individuales de un cluster
const puntosDelCluster = index.getLeaves(
  cluster.properties.cluster_id, 
  Infinity  // cantidad de puntos a obtener
);

console.log(puntosDelCluster);

Recursos