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
npm install supercluster
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
- Formato de coordenadas: Supercluster usa
[lng, lat], no[lat, lng]como Leaflet. - Reindexar datos: Si tus datos cambian, llama a
index.load(nuevosDatos)de nuevo. - getLeaves(): Usa esta función para obtener todos los puntos dentro de un cluster.
- 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);
