Show and Tell - Dash Leaflet

@tracek Per default, the tooltip property is rendered as tooltip, but you can render a customized tool tip by providing a custom onEachFeature function. There is an example in the scatter plot tutorial section,

from dash_extensions.javascript import assign
...
on_each_feature = assign("""function(feature, layer, context){
    layer.bindTooltip(`${feature.properties.city} (${feature.properties.density})`)
}""")
...
geojson = dl.GeoJSON(url="/assets/us-cities.json",
                     onEachFeature=on_each_feature,  # add (custom) tooltip
...

For your use case, you’d need to change the function slightly, I guess to something like,

dl.GeoJSON(..., onEachFeature=assign("function(feature, layer, context){layer.bindTooltip(feature.properties.name)}"))

Perfect, thanks, that was exactly what I needed. Is it also most efficient solution with large number of points? Or should hideout be used?

One extra question: in the migration guide you mention dropping support for geobuf, but it seems to be working for me with the most recent version (1.0.8). Should we expect that it stops working at any time (or maybe it’s working by accident) or the doc is incorrect?

It is the most efficient solution. And regarding geobuf - I was considering dropping support in favor of flatgeobuf, but I have instead decided til add flatgeobuf (and keep geobuf). I have updated the migration notes accordingly :slight_smile:

Hi @Emil,
big fan of dash-leaflet!

I want my map to focus on markers added to its LayerGroup upon clicking a button. In previous version, I managed to do this using (‘map’,‘bounds’) in the callback output. However, with version 1.0.8 I cannot manage to get this to work anymore.

Can you share any advise on how I can achieve this in the latest version?

Could you post what is not working? I believe bounds tracking should work with 1.0.8. There was an issue with center/zoom tracking though, which should be fixed in 1.0.9.

Sure, in the minimal example below, I’m trying to set the app bounds to the specified values, but that does not seem to have an effect. Thanks for your help.

import dash_leaflet as dl
import dash_bootstrap_components as dbc
from dash import Dash, Input, Output, State

app = Dash()

app.layout = dbc.Container([
    dl.MapContainer([
        dl.TileLayer(),
    ],
    center=[56,10], zoom=10,
    id='map',
    style={'width':'100%','height':'80vh'}),
    dbc.Button(
        "Zoom to bounds",
        n_clicks=0,
        id='btn-zoom'
    )
])

@app.callback(
    Output('map','bounds'),
    Input('btn-zoom','n_clicks')
)
def zoom_to_bounds(n_clicks):
    return ([54.8, 10.2], [57.8,21.2])

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

Ah, I though you were taking about reading the bounds of the map. In the latest version, manipulation of the viewport happen via the viewport property. I made this design change to enable adding information on how the viewport changes should happen (e.g. by flying, panning, etc.). If you don’t specify anything, the behaviour will be as before (i.e. the map just snaps to bounds or center/zoom).

Here is a small example that should run with the 1.0.10 release,

from dash import Dash, html, Output, Input
import dash_leaflet as dl

app = Dash()
app.layout = html.Div([
    dl.Map(dl.TileLayer(), style={'height': '50vh'}, center=[56, 10], zoom=6, id="map"),
    html.Button("TRIGGER", id="btn")
])

@app.callback(Output("map", "viewport"),
              Input("btn", "n_clicks"),
              prevent_initial_call=True)
def update(_):
    return dict(bounds=[[42, 10], [44, 8]], transition="flyToBounds")

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

Hi @Emil

Sorry If you are tired of hearing this but first of all thank you so much for Dash Leaflet. Not only you have built an awesome map solution for Dash users but the documentation and guides are awesome. As if this wouldn’t be enought, your quick and continuous support here on the Show an Tell is outstanding!

If you could help me, my question here is about the “dashExtensions_default.js” file that is automatically created at the assets folder. I guess normally I wouldn’t dive into this, but as I’m getting some error messages I tried to understand what’s going on.

What I know by reading the tutorials is that:

A) In dash-leaflet , some component properties are functions . A function handle cannot be passed directly as a property in Dash, but the dash-extensions library provides a few implicit options.

B) Under the hood, the inline assign functions are transpiled into a .js file, which is written to the assets folder.

Ok so suppose I have my .py file where I have:

...
eventHandlers = dict(
    mousemove=assign("function(e, ctx){ctx.setProps({custom_tag: e.latlng})}")
)
...
dl_map = Map(children=[TileLayer()], eventHandlers=eventHandlers, ...

So far so good. What I’m doing is:

i) Using eventHandlers property to provide an interface to inject custom event handlers
ii) Listening for a mousemove event, and throwing some information to Dash through my custom_tag, which I can access in a standard Dash callback.

Ok, then the first time I execute this python script and inspect my .js file that was automatically created at my assets folder, I see:

window.dashExtensions = Object.assign({}, window.dashExtensions, {
    default: {
        function0: function(e, ctx) {
            ctx.setProps({
                custom_tag: e.latlng
            })
        }
    }
});

Without changing absolutely anything, the second time I execute my file and inspect my .js file, I see:

window.dashExtensions = Object.assign({}, window.dashExtensions, {
    default: {
        function0: function(e, ctx) {
            ctx.setProps({
                custom_tag: e.latlng
            })
        },
        function1: function(e, ctx) {
            ctx.setProps({
                custom_tag: e.latlng
            })
        }
    }
});

So…why every time I run the python.py script there is a new function added to the .js file?

The first time I run the .py script the .js file contains ‘function0’.
The second time I run the .py script the .js file contains ‘function0’ and ‘function1’.
The third time I run the .py script the .js file contains ‘function0’, ‘function1’ and ‘function2’, and so on.

All the functions containing exactly the same body and doing the same thing.

After some hours of testing and dubuging I think this is why sometimes I’m getting errors like “No match for [dashExtensions.default.function5] in the global window object.”

So what makes sense in my mind and format my question is: how can I (If I can) write a ‘fixed’ .js file with named functions, so that when I run my .py script the function/file will be read?

I don’t know JavaScript so I don’t know If what I’m asking is feasible, but I imagine something like:
(The only change made is changing from function0 to my_named_function)

window.dashExtensions = Object.assign({}, window.dashExtensions, {
    default: {
        my_named_function: function(e, ctx) {
            ctx.setProps({
                custom_tag: e.latlng
            })
        }
    }
});

and then at my Python script instead of assigning a new function, I could just read it:

eventHandlers = dict(
    mousemove=read("my_named_function")
)
...
dl_map = Map(children=[TileLayer()], eventHandlers=eventHandlers, ...

While using/creating the app maybe there is no big deal with every time creating a function0, function1, etc, but when deploying my app I imagine I’ll must have a ‘fixed’ .js file that will tell Dash what to do for every event I want to listen.

Big thank you!

Thank you for the kind words :smiley:

Yes, the way that the inline assignments of JS functions work is that they are written to assets/dashExtensions_default.js, which is then picked up by the app on initialization. In the current implementation, the function is written when the assign function is called.

Assuming that the call happens only once (which is the case in the examples, where the assign calls are in the global scope, but wouldn’t be the case if assign was invoked from e.g. a callback), each function should only be written once. I just tried testing on the latest release (1.0.3), and I didn’t get any duplicate functions (as you mention) by running the app multiple times.

Do you see the issue with the first example in the docs? If so, could you provide som details on how you run the app (IDE, debugging flags/tool etc.)? If you do not, could you provide a MWE that demonstrates the issue?

Hi @Emil !

Thank you for the quick reply.

No, I don’t see the issue with the first example in the docs. I can run the app multiple times and the .js file is maintained with only the code I wrote initially, which is the one shown in the docs:

window.myNamespace = Object.assign({}, window.myNamespace, {  
    mySubNamespace: {  
        pointToLayer: function(feature, latlng, context) {  
            return L.circleMarker(latlng)  
        } 
    }  
});

Again, I don’t know JavaScript but what I can understand is that I’m not assigning a new function each time the app runs, instead I’m reading it with:

from dash_extensions.javascript import Namespace
ns = Namespace("myNamespace", "mySubNamespace")

So nothing is overwritten.

Below you can see a MWE that demonstrates my issue:

from dash import Dash
import dash_leaflet as dl
from dash_extensions.javascript import assign

eventHandlers = dict(
    click=assign(
        "function(e, ctx){ctx.setProps({any_tag_name: 'pass data to Dash here'})}"
    ),
)

# Create the app.
app = Dash()

app.layout = dl.Map(
    [dl.TileLayer()], 
    eventHandlers=eventHandlers,
    center=(0, 0), 
    zoom=5, 
    style={'height': '50vh'}
)

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

Each time I run the code above one function is created on the .js file, as I mentioned earlier.
First time contains:

window.dashExtensions = Object.assign({}, window.dashExtensions, {
    default: {
        function0: function(e, ctx) {
            ctx.setProps({
                any_tag_name: 'pass data to Dash here'
            })
        }
    }
});

Second time:

window.dashExtensions = Object.assign({}, window.dashExtensions, {
    default: {
        function0: function(e, ctx) {
            ctx.setProps({
                any_tag_name: 'pass data to Dash here'
            })
        },
        function1: function(e, ctx) {
            ctx.setProps({
                any_tag_name: 'pass data to Dash here'
            })
        }
    }
});

and so on.
I’m using Spyder as my IDE and Dash Leaflet at version 1.0.10.

I think I just need to know how to write the JS file for each custom event I’m looking for?

It turns out that during writting this reply and after reading again and again the docs and your answer I was able to sucessfully address my issue! =)

So now, for example, I can replace the MWE I wrote above by this one:

from dash import Dash, Output, Input, html
import dash_leaflet as dl
from dash_extensions.javascript import assign

# Create the app.
app = Dash()

app.layout = html.Div(
    children = [
        html.Div(
            id='debug'
        ),
        dl.Map(
            id='map',
            children=[dl.TileLayer()], 
            center=(0, 0), 
            zoom=5, 
            style={'height': '50vh'}
        )
    ]
)

@app.callback(
    Output('debug', 'children'),
    Input('map', 'clickData')
)
def test(clickData):
    return str(clickData)

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

and save this .js file to me assets folder:

window.myNamespace = Object.assign({}, window.myNamespace, {  
    mySubNamespace: {  
        click: function(e: LeafletMouseEvent, ctx) {
   	    ctx.setProps(
                {
                    n_clicks: ctx.n_clicks == undefined ? 1 : ctx.n_clicks + 1,  // increment counter
                    clickData: {
                        latlng: e.latlng,
                        layerPoint: e.layerPoint,
                        containerPoint: e.containerPoint
                    }  
                }
            )  
        }
    }  
})

and I can keep adding events, like a double click event:

window.myNamespace = Object.assign({}, window.myNamespace, {  
    mySubNamespace: {  
        click: function(e: LeafletMouseEvent, ctx) {
   	    ctx.setProps(
                {
                    n_clicks: ctx.n_clicks == undefined ? 1 : ctx.n_clicks + 1,  // increment counter
                    clickData: {
                        latlng: e.latlng,
                        layerPoint: e.layerPoint,
                        containerPoint: e.containerPoint
                    }  
                }
            )  
        },
        dblclick: function(e: LeafletMouseEvent, ctx) {
   	    ctx.setProps(
                {
                    n_clicks: ctx.n_clicks == undefined ? 1 : ctx.n_clicks + 1,  // increment counter
                    dblclickData: {
                        latlng: e.latlng,
                        layerPoint: e.layerPoint,
                        containerPoint: e.containerPoint
                    }  
                }
            )  
        }
    }  
})

So I think my question have been answered! Thanks @Emil .

Before I go out there saying I know JavaScript would you know why my mouse move event if not firing the callback? I wrote the code on the third function below:

window.myNamespace = Object.assign({}, window.myNamespace, {  
    mySubNamespace: {  
        click: function(e: LeafletMouseEvent, ctx) {
   	    ctx.setProps(
                {
                    n_clicks: ctx.n_clicks == undefined ? 1 : ctx.n_clicks + 1,  // increment counter
                    clickData: {
                        latlng: e.latlng,
                        layerPoint: e.layerPoint,
                        containerPoint: e.containerPoint
                    }  
                }
            )  
        },
        dblclick: function(e: LeafletMouseEvent, ctx) {
   	    ctx.setProps(
                {
                    n_clicks: ctx.n_clicks == undefined ? 1 : ctx.n_clicks + 1,  // increment counter
                    dblclickData: {
                        latlng: e.latlng,
                        layerPoint: e.layerPoint,
                        containerPoint: e.containerPoint
                    }  
                }
            )  
        },
        mousemove: function(e: LeafletMouseEvent, ctx) {
   	    ctx.setProps(
                {
                    n_clicks: ctx.n_clicks == undefined ? 1 : ctx.n_clicks + 1,  // increment counter
                    mousemoveData: {
                        latlng: e.latlng,
                        layerPoint: e.layerPoint,
                        containerPoint: e.containerPoint
                    }  
                }
            )  
        }
    }  
})

and in the callback I’m listening to it:

@app.callback(
    Output('debug', 'children'),
    Input('map', 'mousemoveData')
)

But the ‘children’ of the ‘debug’ component is not updating.

Big thanks again.

This did the trick. Thanks a lot for the assistance! :slight_smile:

1 Like

Great to hear that you got it working! I stil haven’t been able to reproduce the issue that you were facing, but I have come up with a possible fix. Would you be willing to test it out? It should be available, if you install the latest rc release,

pip install dash-extensions==1.0.4rc5

Hey @Emil !

Sure! I’m always willing to try out new possibilities!

I did installed dash-extensions==1.0.4rc5 and yes, It apparently solved the issue! The .js file is no longer being written with duplicated functions!

One last thing and I’ll leave you alone?! :slight_smile:

Considering that I was able to reproduce the first example in the docs, how can I add to this code below an event handler to hear to the mouse move event?

Original code in the docs that I was able to reproduce:

window.myNamespace = Object.assign({}, window.myNamespace, {  
    mySubNamespace: {  
        pointToLayer: function(feature, latlng, context) {  
            return L.circleMarker(latlng)  
        }  
    }  
});

I imagine that adding the mouse move handler would be something like:

window.myNamespace = Object.assign({}, window.myNamespace, {  
    mySubNamespace: {  
        pointToLayer: function(feature, latlng, context) {  
            return L.circleMarker(latlng)  
        }  
    },
    mousemove: function(e: LeafletMouseEvent, ctx) {
        ctx.setProps({
            mousemoveData: {
                latlng: e.latlng,
                layerPoint: e.layerPoint,
                containerPoint: e.containerPoint
            }
        });
    }  
});

and then in Dash:

...
@app.callback(
    Output(...),
    Input('map', 'mousemoveData')
)
...

But I’m not understanding how to connect the handler to Dash…maybe I need to something like below (and further)?

from dash_extensions.javascript import Namespace
ns = Namespace("myNamespace", "mySubNamespace")

Thanks!

You need to register the eventhander, see this section for details :blush:

https://www.dash-leaflet.com/docs/events#a-custom-event-handlers

Is it possible to copy marker’s tooltip text to a clipboard? That is, looking at Clipboard API, is it possible to override behaviour in a way that clicking the point copies the information?

Hi @Emil ,

thank you for your great work!

Is it possible to change the language of MeasureControl? It is possible to do so with leaflet-measure, they have many json files with different languages for the text snippets ( github ). Can we access them somehow?

Thank you
Max

Hi gang & @Emil
Firstly I love the Dash leaflet implementation and thanks for your hard work on this. The latest versions seems to have lots of cool features I need to try out :slight_smile:
However I think I’ve found a possible bug in Dash Leaflet or at least a strange behaviour. Is this the right place to discuss?

I was previously using dash_leaflet==0.1.23 for some time and decided to upgrade my environment to the latest version as of now e.g., dash_leaflet==1.0.11.

Suspected bug:

I have a dl.Geojson with some point features on the map. Each feature I have a given a property called “Icon” which has contains a string of the icon path (i.e., locally in assets folder) of the icon to use for the map. This works as expected.
The problem arises if you then go and send an updated version of the geojson data to the data attribute of the dl.Geojson. I.e., to change the icons. I note the icons do not change. Initially I thought I had coded something wrong, but eventually I noticed that the icons are changing, but the change is only visible when you pan away from the area and then re-pan back to where the icons are. In the previous version there was an instantaneous update of the icons but now they only update once you pan the map away and back again.

Thanks again and interested to hear your thoughts

Hi @dsmi90

Thank you for the nice words. The naming convention changes were made in the (breaking) 1.0.0 release to align more closely with the Dash core libraries. Regarding the suspected error - I had to rewrite all of the GeoJSON rendering logic as part of the update, so it is not unlikely that a bug has been introduced. Could you post a MWE (a small runnable example) that demonstrates the issue? Then I’ll take a look when I have some spare time. The proper place would be on GitHub (then it’s easier to track progress on the issue), but here would also be OK :slight_smile:

2 Likes

Hi again & thank you for quick response. After some experimenting I was able to replicate the behaviour. It seems to occur only when clustering is turned on for the geojson layer and the icons are updated. Example code is provided below:

"""Example of Dash Leaflet Bug."""
import dash_extensions.javascript as djs
import dash_leaflet as dl
import dash_leaflet.express as dlx
from dash import Dash, html
from dash.dependencies import Input, Output, State

point_to_layer = djs.assign(
    """function(feature, latlng){
    const flag = L.icon({
        iconUrl: `${feature.properties.Icon}`,
        iconSize: [40, 40],
    });
    return L.marker(latlng, {icon: flag});
}"""
)

geojson = dl.GeoJSON(
    data=dlx.dicts_to_geojson(
        [
            {"lat": 56, "lon": 10, "Icon": "/assets/static/green.png"},
            {"lat": 57, "lon": 10, "Icon": "/assets/static/green.png"},
        ]
    ),
    id="geojson_id",
    options=dict(pointToLayer=point_to_layer),
    cluster=True,
)

app = Dash()
app.layout = html.Div(
    [
        dl.Map(
            [dl.TileLayer(), geojson], center=[56, 10], zoom=6, style={"height": "50vh"}
        ),
        html.Button("Toggle Cluster", id="toggle_cluster", n_clicks=0),
        html.Button("Update Icons", id="change_icons_btn", n_clicks=0),
    ]
)

@app.callback(
    Output("geojson_id", "cluster"),
    Input("toggle_cluster", "n_clicks"),
    prevent_inital_call=True,
)
def toggle_cluster_on_off(n_clicks):
    if n_clicks % 2 == 0:
        return True
    return False


@app.callback(
    Output("geojson_id", "data"),
    Input("change_icons_btn", "n_clicks"),
    State("geojson_id", "data"),
    prevent_inital_call=True,
)
def change_map_icons(n_clicks, geojson_data):
    if n_clicks % 2 == 0:
        icon = "/assets/static/green.png"
    else:
        icon = "/assets/static/red.png"

    for point in geojson_data["features"]:
        point["properties"]["Icon"] = icon

    return geojson_data


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

I suspect a temporary workaround could be implemented by using a callback to force clustering to turn off at a given zoom level.

Thanks again :slight_smile:

@Emil Sorry for the pile-on but while on the topic of migrating to latest dash_leaflet version I think I’ve found another inconsistency in behaviour - I am no longer able to alter the edit_toolbar draw options after initialising the map. In previous version this worked as expected but now appears to have stopped working.

import dash_leaflet as dl
from dash import Dash, html
from dash.dependencies import Input, Output

markers = ["circle", "marker", "polyline", "polygon", "rectangle", "circlemarker"]
all_tools_off = {key: False for key in markers}
all_tools_on = {key: True for key in markers}

app = Dash()
app.layout = html.Div(
    [
        dl.Map(
            [
                dl.TileLayer(),
                dl.FeatureGroup(dl.EditControl(id="edit_control", draw=all_tools_off)),
            ],
            center=[56, 10],
            zoom=6,
            style={"height": "50vh"},
        ),
        html.Button("Change Draw Toolbar", id="button", n_clicks=0),
    ]
)

@app.callback(
    Output("edit_control", "draw"),
    Input("button", "n_clicks"),
    prevent_initial_call=True,
)
def change_draw_options(n_clicks):
    if n_clicks % 2 == 0:
        return all_tools_off
    else:
        return all_tools_on

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

If I find any more i’ll try work out how to report them through the correct channel…
Cheers from a big fan of dash leaflet :slight_smile: