✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
🧬 Learn how to build RNA-Seq data apps with Python & Dash. Register for the May 20 Webinar!

Efficient clustering on map?

Hi
I have a relatively straightforward problem: draw points on the map and do some action when a user clicks on any of the points. I have implemented it in two ways, first with MapBox and the other with Leaflet.
The Mapbox version works otherwise well, but I’m missing the clustering option and I cannot find an example for it as well. I’m sure it is possible with some custom work, but as I’m not a Javascript person, I do not have the resources to dig into it.
The leaflet version looks great, does not require registering, but from some number of stations, it becomes slow. Already with 3000 points, it takes several seconds to just parse the click, which is unacceptable.

Does anyone have an efficient way of having points clustered on a map and fast callback after clicking on a point?

The bare-bones Leaflet example is described here, which is a bit faster than my actual deployment, but you can still see it getting slow with approx 10,000 points. https://stackoverflow.com/questions/62334801/dash-leaflet-get-marker-location

Have you tried passing the preferCanvas=True option to the Leaflet map? It should improve the performance if you have a large number of markers.

No, I had not tried it before but tried now

        html.Div(
            dl.Map([dl.TileLayer(),
                    dl.LayerGroup(id="layer"),
                    cluster
            ],
                   id = 'tab1-leaflet',
                   preferCanvas=True,
                   zoom=2,
                   center=[30,-90],
                   style={'width': '45%',
                          'height': '50vh',
                          'margin': "auto",
                          "display": "inline-block"})),

However, this does not seem to have significant effect, did I put it in a right place?

IMO the problem lies in the way I parse the markers, is there another way of doing this?

@dash_app.callback(
    Output('dropdown', 'value'),
    [Input(marker.id, "n_clicks") for marker in markers])

Yes, it looks right (here is an example for reference).

In fact, there are multiple issues when you go to > 1000 markers. One is that each marker is inserted into the DOM (which should be fixed by the preferCanvas=True as far as i understand). The event handling itself might also be problematic, i will need to do some testing to be sure.

Thanks. I checked your example and tried to turn on/off the preferCanvas=True. It seems to have an effect on the overall behaviour of the page, zoom-in zoom-out etc, i.e. it gets smoother with preferCanvas, but I do not feel a big difference in callback speed? In my case, the page is very nice and responsive.

No, i don’t think that the option will affect callback speed significantly. It will main help on map rendering performance and browser memory usage. Since you are already using clusters, this might not be the much of an issue anyhow.

I just tried delegating the event handling to the map cluster component itself. With this control flow, you limit the number of inputs to your callback to single one. The previous example would be,

import json
import random
import dash
import dash_html_components as html
import dash_leaflet as dl
from dash.dependencies import Output, Input

n = 10000
lats = [random.randrange(44000, 48000) / 1000.0 for i in range(n)]
lons = [random.randrange(0, 4000) / 1000.0 for i in range(n)]
markers = [dl.Marker(id=str(i), position=(lats[i], lons[i])) for i in range(n)]
cluster = dl.MarkerClusterGroup(children=markers, id="cluster")

app = dash.Dash(external_stylesheets=['https://codepen.io/chriddyp/pen/bWLwgP.css'], prevent_initial_callbacks=True)
app.layout = html.Div([
    dl.Map(children=[dl.TileLayer(url="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"), cluster],
           style={'width': "100%", 'height': "400px"}, center=[46, 2], zoom=7, id="map", preferCanvas=True), html.P(id="log")],
    style={"position": "relative", 'width': '1000px', 'height': '300px'})


@app.callback(Output("log", "children"), [Input("cluster", "click_marker_id")])
def marker_click(marker_id):
    return marker_id

if __name__ == '__main__':
    app.run_server(debug=False)

I just tried it out on my desktop PC (which is pretty powerfull). With 10.000 markers, the click event takes about 300 ms to execute compared to around 1300 ms for the solution i posted previously. Hence it’s better, but not great. If you want to try it out, you can install it as

pip install dash-leaflet==0.0.18rc2

Thanks, this actually seems quite a bit faster, maybe 30-40% in my setup and with a non-exact measurement. So I can continue using Leaflet in my development at least. However, if you have any ideas how to make it even faster, it would be very nice.

Great! To get even more efficient clustering, i think the next step would be to change the clustering component to a more effective alternative such as PruneCluster. It will require some work though, especially since there is no PruneCluster React component.