GeoJSON clustering

SuperCluster-backed point clustering for dl2.GeoJSON — cluster, pointToLayer, clusterToLayer, hideout, superClusterOptions.

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 = '<div class="dl2-cluster-bubble" style="background:' + color + 'cc;color:#fff;font-weight:700;">' + count + '</div>'; 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*

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: