Updating folium coordinates on callback in dash app

I am trying to update the coordinates of a folium map rendered by a Dash App on a callback (which is running inside a Flask App). The callback selectively renders different layers on a map - the issue is that the zoom and center coordinates are not persisted when the map is updated. The map is rendered as html and injected as in iframe into the app.

Addendum: Not a professional programmer, have only been trying my hand at this for the past six months.

I have tried three approaches:

  1. JS API call client-side to flask route. I ended up realizing this had too much overhead (plus couldn’t identify user to update the proper coordinates).
  2. Encoding the coordinates and zoom in the URL. The URL changes as expected from this js snippet:
map_layer.on("mouseup zoomend", function(){
                    
   var coordinates = map_layer.getCenter();
   var lat = coordinates.lat
   var lon = coordinates.lng
   var zoom = map_layer.getZoom();
                    
   parent.history.replaceState({}, '', `/app/layer?&lat=${lat}&lon=${lon}&zoom=${zoom}`);
   // const ON_CHANGE = '_dashprivate_historychange';
   // window.dispatchEvent(new CustomEvent(ON_CHANGE));
   // parent.window.dispatchEvent(new CustomEvent(ON_CHANGE));

                    
   console.log("success");

The commented-out code also tries to dispatch a CustomEvent to dash to try and update it’s history - not really clear what’s happening there - just tried to emulate this approach.

The right URL is not passed on to the callback however. So despite the url changing on the browser, it’s not being sent back with the updated query variables. If I refresh the page and actively send the URL, than the right URL is passed on, but that’s not the kind of behavior I’m looking for - I would like for it to change with no active reloading of the page:

@layer.callback(
    Output("map", "srcDoc"),
    -- other inputs --,
    Input('url', 'href') #tried State as well
)
def update_output_src(href):
    print(href)
    
    -- other code --
    
    return map.get_root().render()
  1. Using a hidden DIV to store the coordinates . The div content changes as expected from this js snippet:
map_layer.on("mouseup zoomend", function(){

   var coordinates = map_layer.getCenter();
   var lat = coordinates.lat
   var lon = coordinates.lng
   var zoom = map_layer.getZoom();

   var latstore = parent.document.getElementById('latstore');
   latstore.textContent = lat; #trying different approaches in what's changed to see if dash callback captures it.
                    
   var lonstore = parent.document.getElementById('lonstore');
   lonstore.innerText = lon;
                    
   var zoomstore = parent.document.getElementById('zoomstore');
   zoomstore.innerHTML = zoom;

   console.log("success");

But again I am not able to capture the stored coordinates when the input is triggered.

@layer.callback(
    Output("map", "srcDoc"),
    -- other inputs --,
    State('latstore', 'children'),
    State('lonstore', 'children'),
    State('zoomstore', 'children'),
)
def update_output_src(latstore, lonstore, zoomstore):
    print(latstore, lonstore, zoomstor)
    
    -- other code --
    
    return map.get_root().render()

Any help or pointer in the right direction for approaches 2 or 3 would be super useful. I have been struggling with this for 3-4 days now and I’m out of ideas.

Have you considered using dash-leaflet?

https://dash-leaflet.herokuapp.com/

1 Like

I have, but from what I understand the only map events exposed through dash leaflet are mouse click and mouse double click (which are not fit for my purpose and therefore I guess I would still need to find a similar hack to the challenge I’m having). But thank you for your answer maybe there’s something in the exposed mouse click event that I can learn from!

What events do you need? If it’s just the properties of the viewport, you can get them via the viewport property (and/or the bounds property, depending on what info you need).

EDIT: It could look like you are manually setting up tile layer rendering. If that’s the case, I would recommend to use standard functionality instead.

1 Like

I’m basically trying to persist the map by getting the zoom and viewport center values client-side and then feeding them back to the callback that generates the map with the selected layers (there’s more than a hundred options and and they are called on the fly from different services, so I can’t render all of them at the beginning and then hide/show them through layer control).

Any idea how I can get updated viewport bounds/properties as variables server-side through dash-leaflet?

Thank you very much for your help!!

Wouldn’t the viewport property be sufficient then?

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

# Create app.
app = Dash()
app.layout = html.Div([dl.Map(children=[dl.TileLayer()], id="map"), html.Div(id="log")],
                      style={'width': '100%', 'height': '50vh', 'margin': "auto", "display": "block"})


@app.callback(Output("log", "children"), Input("map", "viewport"))
def log_viewport(viewport):
    print(viewport)
    return json.dumps(viewport)


if __name__ == '__main__':
    app.run_server(port=9999)
2 Likes

Perfect! Thank you!

Hi again @Emil

I’m sorry for bothering you one more time :slight_smile: ! Dash-leaflet is awesome and it’s going to be my go to place going forward. I have been trying to refractor the current project but I’m encountering numerous obstacles (no heatmap component yet, reimplementing all legends from branca etc.) So after going at it for a week I wanted to see if there’s any high level solution you might suggest, to implement a viewport OR coordinates+zoom+currentbasemap callback which would not require me to redo the whole project on dl (there’s approx. 80 different layers, most of the data is already compressed and in a specific json format which is not working properly with dl etc.).

Is the only viable solution to get this to make a custom dash component that connects getCenter() / getZoom() to a dash output? Is there any simpler way to do this (I was looking at the namespace feature in dl documentation but I’m really out of my depth here :slight_smile:

Thanks again for any help!!

When I first started working with maps in Dash, I looked briefly at Folium, but I decided relatively early to make a custom component (i.e. dash-leaflet) instead of trying to build an integration with Folium. My conclusion was that the architectures are too different to get a good two-way binding experience without too much work. So unfortunately, I won’t be if much help in that regard :slightly_smiling_face:

1 Like