# GeoJSON clustering > SuperCluster-backed point clustering for dl2.GeoJSON — cluster, pointToLayer, clusterToLayer, hideout, superClusterOptions. --- .. llms_copy::GeoJSON clustering .. toc:: ### Overview dash-leaflet 1.x lets `GeoJSON` collapse dense point sets into clusters that expand on zoom. dl2 now does the same — `dl2.GeoJSON(cluster=True, ...)` runs the [SuperCluster](https://github.com/mapbox/supercluster) index that dash-leaflet 1.x uses, and accepts the same customization hooks: | Prop | What it does | |-----------------------|--------------| | `cluster` | Turn clustering on/off. | | `superClusterOptions` | `{radius, minPoints, maxZoom, minZoom, extent}` — tuning passed to SuperCluster. | | `pointToLayer` | JS function source `(feature, latlng, ctx) => layer` for individual points. | | `clusterToLayer` | JS function source `(feature, latlng, index, ctx) => layer` for cluster bubbles. | | `hideout` | `dict` passed to your JS as `ctx.hideout` — color maps, label dicts, anything. | | `zoomToBoundsOnClick` | Click a cluster to fly the camera to fit its children. | The JS function source is wrapped in `new Function(...)` at construction time. `ctx` carries `{ hideout, leaflet, map }` so your function can build any Leaflet 2 layer without depending on a global. ### Live demo 200 random "vessel positions" around San Diego, CA, colored by category. Pan out and they collapse into glass bubbles; pan in and they expand. Click a cluster and you fly to its children's bounding box. .. exec::docs.geojson-cluster.example :code: false ### Source ```python # File: docs/geojson-cluster/example.py """ GeoJSON clustering — limited working example. 200 random vessel-position points around San Diego, CA. The hideout dict ships a {category: color} map into the JS pointToLayer so circles paint without a Python round-trip. Slider on the right tunes superClusterOptions.radius live. """ import json import random import dash_leaflet2 as dl2 import dash_mantine_components as dmc from dash import Input, Output, callback, html from dl2_tiles import OCEAN, register_theme_swap from dl2_locations import SAN_DIEGO from dl2_shared import code_panel, header, info_panel # Basemap pair for this page — dl2_tiles owns the light/dark wiring. TILES = OCEAN CATEGORIES = ["fishing", "sailing", "ferry", "cargo"] COLORS = { "fishing": "#4dabf7", "sailing": "#69db7c", "ferry": "#ffd43b", "cargo": "#ff8787", } def make_points(n=200, seed=42): rng = random.Random(seed) features = [] for i in range(n): # Scatter in kilometres, not degrees: a fixed degree jitter would # produce an east-west-stretched blob at low latitudes and a # squashed one up north. +/- 11 km N-S by +/- 13 km E-W. lat, lng = SAN_DIEGO.at( north_km=(rng.random() - 0.5) * 22.2, east_km=(rng.random() - 0.5) * 25.0, ) category = rng.choice(CATEGORIES) features.append( { "type": "Feature", "geometry": {"type": "Point", "coordinates": [lng, lat]}, "properties": { "id": i, "category": category, "name": f"{category.title()} #{i}", }, } ) return {"type": "FeatureCollection", "features": features} POINTS = make_points() # JS source — `new Function('return (' + source + ')')()` is called per prop. POINT_TO_LAYER = """ function (feature, latlng, ctx) { var color = (ctx.hideout && ctx.hideout.colors) ? ctx.hideout.colors[feature.properties.category] || '#868e96' : '#228be6'; return new ctx.leaflet.CircleMarker(latlng, { radius: 6, color: color, weight: 1.5, fillColor: color, fillOpacity: 0.85 }); } """ CLUSTER_TO_LAYER = """ function (feature, latlng, index, ctx) { var count = feature.properties.point_count; var leaves = index.getLeaves(feature.properties.cluster_id, Infinity); var counts = {}; for (var i = 0; i < leaves.length; i++) { var c = leaves[i].properties.category; counts[c] = (counts[c] || 0) + 1; } var top = Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a]; })[0]; var color = (ctx.hideout && ctx.hideout.colors && ctx.hideout.colors[top]) || '#228be6'; var size = count >= 100 ? 56 : count >= 10 ? 44 : 36; var html = '
' + count + '
'; return new ctx.leaflet.Marker(latlng, { icon: new ctx.leaflet.DivIcon({ html: html, className: '', iconSize: [size, size], iconAnchor: [size / 2, size / 2] }) }); } """ CODE = """dl2.GeoJSON( id="cluster-geo", data=feature_collection, # 200 vessel points cluster=True, superClusterOptions={"radius": 80, "minPoints": 2, "maxZoom": 16}, zoomToBoundsOnClick=True, hideout={"colors": {"fishing": "#4dabf7", "sailing": "#69db7c", ...}}, pointToLayer=''' function (feature, latlng, ctx) { const color = ctx.hideout.colors[feature.properties.category] || '#868e96'; return new ctx.leaflet.CircleMarker(latlng, {radius: 6, color, fillOpacity: 0.85}); } ''', clusterToLayer=''' function (feature, latlng, index, ctx) { const count = feature.properties.point_count; // ... figure out the dominant category from the cluster's leaves, // build a colored DivIcon return new ctx.leaflet.Marker(latlng, {icon: ...}); } ''', )""" component = dmc.Stack( [ header( "GeoJSON clustering", "200 vessel points around San Diego, CA. The hideout dict ships a " "color map into the JS — clusters take the dominant category's " "color. Pan/zoom and the SuperCluster index re-renders for the " "new viewport.", badge="dl2.GeoJSON", ), dmc.Grid( [ dmc.GridCol( dmc.Paper( dl2.Map( id="cl-map", center=SAN_DIEGO.center, zoom=10, style={"height": "60vh"}, children=[ dl2.TileLayer(id="cl-tile", **TILES.kwargs("light")), dl2.GeoJSON( id="cl-geo", data=POINTS, cluster=True, superClusterOptions={ "radius": 80, "minPoints": 2, "maxZoom": 16, }, zoomToBoundsOnClick=True, hideout={"colors": COLORS}, pointToLayer=POINT_TO_LAYER, clusterToLayer=CLUSTER_TO_LAYER, ), ], ), shadow="sm", radius="md", withBorder=True, style={"overflow": "hidden", "height": "60vh"}, ), span=8, ), dmc.GridCol( dmc.Stack( [ info_panel( "Cluster radius (px)", dmc.Slider( id="cl-radius", min=20, max=160, step=10, value=80, marks=[ {"value": 20, "label": "20"}, {"value": 80, "label": "80"}, {"value": 160, "label": "160"}, ], ), ), info_panel( "Categories", html.Div( [ dmc.Group( [ html.Div( style={ "width": "14px", "height": "14px", "borderRadius": "50%", "background": COLORS[c], } ), dmc.Text(c.title(), size="sm"), ], gap="xs", ) for c in CATEGORIES ] ), ), info_panel( "Last click", dmc.Code(id="cl-click-readout", block=True, children="(click a marker or cluster)"), ), ], gap="md", ), span=4, ), ], gutter="md", ), code_panel("dl2.GeoJSON with clustering", CODE), ], gap="md", ) @callback(Output("cl-geo", "superClusterOptions"), Input("cl-radius", "value")) def update_radius(r): return {"radius": int(r or 80), "minPoints": 2, "maxZoom": 16} @callback(Output("cl-click-readout", "children"), Input("cl-geo", "clickFeature")) def show_click(feat): if not feat: return "(click a marker or cluster)" return json.dumps(feat, indent=2) # Light/dark basemap, driven off the color-scheme store. register_theme_swap("cl-tile", TILES) ``` :defaultExpanded: false :withExpandedButton: true --- *Source: /geojson-cluster*