Show and Tell - Dash Leaflet

It is possible to write the Dash wrapper code yourself. However, since the wrapper code is in React, it might take more effort if you only know Python. A good place to start is the docs on custom components,

When you are done with your customizations, you make a PR to the main repo. Here is an example of a such PR,

Hi @Emil,

I’m currently working on a map with approx. 100k markers. To support this number of markers, I opted for the (super)cluster. However, for my use case, the supercluster should not be static, i.e. one should be able to filter the markers based on some included field. Overwriting the GEOJSON data (whilst storing the original data in a cache or hideout) of the supercluster every time you filter is, of course, an option but is not all that performant (~3 seconds).

As such, I opted for custom pointToLayer and clusterToLayer functions, which perform a ā€˜visual’ filter instead of a ā€˜physical’ filter. Below, an example where the points and clusters are filtered based on their ā€˜monetary’ property.

point_to_layer = assign("""function(feature, latlng, context){
    const {bound_monetary} = context.props.hideout;
    if (feature.properties["monetary"] >= bound_monetary) {
        return null;
    }
    return L.circleMarker(latlng) 
}""")
cluster_to_layer = assign("""function(feature, latlng, index, context){
    const {bound_monetary} = context.props.hideout;
    const leaves = index.getLeaves(feature.properties.cluster_id, 20000);
    let valueSum = 0;
    for (let i = 0; i < leaves.length; ++i) {
        if (leaves[i].properties["monetary"] < bound_monetary) {
            valueSum += 1;
        }
    }
    
    if (valueSum == 0){
        return null;
    }

    const icon = L.divIcon.scatter({
        html: '<div style="background-color:white;"><span>' + valueSum + '</span></div>',
        className: "marker-cluster",
        iconSize: L.point(40, 40)
    });
    
    return L.marker(latlng, {icon : icon})
}""")

The filter is now practically instantaneous, but in general scrolling and zooming is slightly slower (~200 ms more than before). Do you see any possibility for further improvement here (i.e. faster filtering/zooming/scrolling)?

@bas_dum Did you try adding a filter as in this example?

http://dash-leaflet.herokuapp.com/#geojson_filter

1 Like

Thanks for the quick response!
Yeah, I did try that, and it worked fine for rendering the individual points, however it no longer showed any clusters. I tried adding cluster=True and superClusterOptions={ā€œradiusā€: 100} as well as cluster=True and superClusterOptions={ā€œradiusā€: 100, ā€˜filter’: geojson_filter, ā€˜hideout’: dd_defaults} to dl.GeoJSON (from the example you mentioned). Moreover, I could not find any example of this behaviour online (having hideout filtering combined with supercluster).

That sounds like a bug. I guess I didn’t test the combination of an interactive filter and clustering properly when I implemented the clustering logic. Hence, I think that with the current version of dash-leaflet, your current solution is the best approach.

Hi Emil, Hi everyone

I have a similar function (code below) where markers are added to a map and I was wondering how to make some of them get deleted when clicked on. I’m struggling to modify the marker_click function above so that when I click on a marker, it is deleted

import dash_html_components as html
import dash_leaflet as dl
from dash.dependencies import Input, Output, State
app = dash.Dash()
app.layout = html.Div([
    dl.Map([dl.TileLayer(), dl.LayerGroup(id="container", children=[])], id="map",
           center=(56, 10), zoom=10, style={'width': '100%', 'height': '50vh', 'margin': "auto", "display": "block"})
])

@app.callback(Output("container", "children"), [Input("map", "click_lat_lng")], [State("container", "children")])
def add_marker(click_lat_lng, children):
    children.append(dl.Marker(position=click_lat_lng))
    return children

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

Any idea or advice ?
Thanks a lot

Hi everyone,

Just thought I would share the project I’ve been working on using the Dash-Leaflet app in Python that plots rocket and drone flight data (GPS, accelerometer, and barometer) from a CSV file. So far I only have the GPS and altitude data plotting nicely there’s a fair bit still to be done.



To do (would love any advice):

  • I’m not sure how to have the plot markers change size as I zoom in/out?
  • I’m not sure how to stop it zooming in too far where the map disappears?
  • I want to have it update as data comes in, so it refreshes when the CSV updates?
  • I’m still not sure how to plot accelerometer data in 3D yet (work in progress)

Thanks for any feedback!

  1. If you use a Circle, it will stay the same size in meters, i.e. it’s rendered size in pixels will change on zoom (on the other hand, a CircleMarker will stay the same size in pixels, i.e. it’s size remains fixed on zoom)
  2. It sounds like you are looking for the maxZoom property of the Map component
  3. You can achieve that behavior by parsing the .csv file on page load, or via a callback
  4. The map itself is 2D, so I am not sure how to achieve a 3D plot. That sounds more like a task for dash-deck
2 Likes

Thanks Emil for so many pieces of advice! I will get to work trying them! Really love this Dash-leaflet project! You’re a legend!

1 Like

First of all, thank you so much Emil for your dedication regarding Dash-Leaflet and your responsiveness here in the Forum!

I have two issues that might be related somehow to rendering, though I do not think they are directly linked.

  • One is the exact same one as user inisamuel posted some time ago (Link). Shapes drawn via EditControl get lost when switching Tabs back and forth. Is there a way to maintain the shapes? (I repost the MWE here.)
import dash
from dash import Dash, dcc, html
import dash_leaflet as dl

app = Dash(__name__)
app.layout = html.Div([
    dcc.Tabs(id="tabs", children=[
        dcc.Tab(value="a", label="a", children=[]),
        dcc.Tab(value="b", label="b", children=[
            dl.Map(center=[56, 10], children=[
                dl.TileLayer(), dl.FeatureGroup([
                    dl.EditControl(id="edit_control")]),
            ], style={'width': '50%', 'height': '50vh'}, id="map"),
        ])
    ])
])

if __name__ == '__main__':
    app.run_server(debug=True)
  • The other one is related to GeoJSON filtering via hideout property PLUS clustering. The MWE is the Interactivity example from the website with two small modifications. The property cluster=True ist added to dl.GeoJSON(), and the javascript function is altered, so that also clusters are shown. One can see that the clusters do not update when removing markers via the Dropdown menu. Is there a way to re-render the cluster markers after filtering the desired properties?
import dash_leaflet as dl
import dash_leaflet.express as dlx
from dash import Dash, html, dcc, Output, Input
from dash_extensions.javascript import assign

# A few cities in Denmark.
cities = [dict(name="Aalborg", lat=57.0268172, lon=9.837735),
          dict(name="Aarhus", lat=56.1780842, lon=10.1119354),
          dict(name="Copenhagen", lat=55.6712474, lon=12.5237848)]
# Create drop down options.
dd_options = [dict(value=c["name"], label=c["name"]) for c in cities]
dd_defaults = [o["value"] for o in dd_options]
# Generate geojson with a marker for each city and name as tooltip.
geojson = dlx.dicts_to_geojson([{**c, **dict(tooltip=c['name'])} for c in cities])
# Create javascript function that filters on feature name.
geojson_filter = assign("function(feature, context){return context.props.hideout.includes(feature.properties.name) || feature.properties.cluster;}")
# Create example app.
app = Dash()
app.layout = html.Div([
    dl.Map(children=[
        dl.TileLayer(),
        dl.GeoJSON(data=geojson, options=dict(filter=geojson_filter), hideout=dd_defaults, id="geojson", zoomToBounds=True, cluster=True)
    ], style={'width': '100%', 'height': '50vh', 'margin': "auto", "display": "block"}, id="map"),
    dcc.Dropdown(id="dd", value=dd_defaults, options=dd_options, clearable=False, multi=True)
])
# Link drop down to geojson hideout prop (could be done with a normal callback, but clientside is more performant).
app.clientside_callback("function(x){return x;}", Output("geojson", "hideout"), Input("dd", "value"))

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

Again, many thanks!

1 Like

Hi would like to ask if it is possible to enable smooth zoom / zoomSnap or fractional zoom in Dash Leaflet as supported in Leaflet 1.x?

Could you link to the specific functionality that you need?

Thank you for the kind words :slight_smile:

  • When you re-render the map, the EditControl state is reset. I haven’t worked persistence myself , but it looks like it might be the proper way to address this issue. However, it would require changes to the dash-leaflet source code. Another solution would be to mirror the data into a Store component (similar to the docs example), and then read the data from the Store when the Tab is rendered. It’s less elegant, but I guess it should work without any changes to the dash-leaflet source code

  • I believe this is a bug. I guess I didn’t test this special case properly when I implemented the cluster rendering logic. Hence, I think the source code of dash-leaflet would need to be updated for it to work

Thanks again for your response.

Would you want me to write two issues on Github to keep track? Also, do you think it is realistic that the issue with filtering and clustering could be solved sometime in the not too distant future?

Thanks for the prompt response.

Excerpt from documentation. Leaflet Zoom

Fractional zoom

A feature introduced in Leaflet 1.0.0 was the concept of fractional zoom. Before this, the zoom level of the map could be only an integer number (0, 1, 2, and so on); but now you can use fractional numbers like 1.5 or 1.25.

Fractional zoom is disabled by default. To enable it, use the map’s zoomSnap option. The zoomSnap option has a default value of 1(which means that the zoom level of the map can be 0, 1, 2, and so on).

If you set the value of zoomSnap to 0.5, the valid zoom levels of the map will be 0, 0.5, 1, 1.5, 2, and so on.

If you set a value of 0.1, the valid zoom levels of the map will be 0, 0.1, 0.2, 0.3, 0.4, and so on.

Ah, yes, now I understand. You should be able to pass that property (i.e. zoomSnap) directly to the Map component,

It worked. Thanks.

Hi everybody, I’m working on a dashboard that allows the user to compare multiple maps that have different geojson layers applied. On initially visiting the dashboard, you are presented with one map at full size. You can then click more maps within the dropdown and they are then all applied to the screen in rows and columns. The problem I’m having is that the original map, while it correctly resizes and fits into the grid, it does not center properly. I’ve created a small mockup that replicates the behavior.

import dash
import dash_leaflet as dl
import dash_core_components as dcc
from dash import html
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc

app = dash.Dash()

app.layout = html.Div([
    html.Button('resize', id='resize'),
    html.Button('apply', id='models-apply'),
    dcc.Tabs(value="tab1",children=[
        dcc.Tab(id='row1',value="tab1",label='Tab one', children=[
            dl.Map(id="map1",children=[dl.TileLayer(id="tile1")],zoom=4, center=(64.1466,21.9426), style={'width': '90vw', 'height': '90vh'}),
            html.P("tab1")
        ])
    ])
])

@app.callback(
        Output('row1', 'children'),
        Input('models-apply', 'n_clicks'),
)
def change_maps(n_clicks):

    if n_clicks==1:
        return dl.Map(id="map1",children=[dl.TileLayer(id="tile1")],zoom=4, center=(64.1466,21.9426), style={'width': '90vw', 'height': '90vh'})
    if n_clicks==2:
        return dl.Map(id="map1",children=[dl.TileLayer(id="tile1")],zoom=4, center=(64.1466,21.9426), style={'width': '40vw', 'height': '40vh'})

    if n_clicks==3:
        return dl.Map(id="map2",children=[dl.TileLayer(id="tile1")],zoom=4, center=(64.1466,21.9426), style={'width': '40vw', 'height': '40vh'})

if __name__ == '__main__':
    app.run_server(host='0.0.0.0',port=8030, debug=True)

Here you can see that a new element is provided by the callback with updated style, zoom, and center. The map’s size updates but it doesn’t bring the center in, where if you change the map’s id, everything works appropriately. Any tips on how to get this working without changing the id?

I believe this behaviour is expected (or at least known). When you replace Leaflet elements in this way (without changing the id), React can get confused, resulting in wrong/missing updates. When you change the ID, React know that it’s a new component, and the update is performed correctly.

For the example at hand, I guess you could instead just target the properties that you want to change (i.e. the viewport?). I believe that should yield the designed result, without the need to create new IDs.

Hi Emil, thanks for getting back to me! In the end, I was able to solve the problem by adding a field to the dictionary that we were using for the ID. If anyone else encounters this issue. I was able to get the maps to update by simply tracking the n_clicks as key/value pair in the ID. Something like this worked perfectly.

figure.id['n_clicks'] = n_clicks