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.

1 Like

Have you considered using dash-leaflet?

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

EDIT: https://dash-leaflet-docs.onrender.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.

2 Likes

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

I switched my app to Dash-Leaflet from Folium and never looking back :vulcan_salute:! Thank you for your work. No need to render and serve heavy html, everything now mostly happens client-side with a huge performance boost.

There are three elements which I still am unable to implement:

  1. Heatmaps (not a big deal tbh)

  2. Legends (folium had a jinja-like package, branca, that could easily overlay a div over the map) using colorbars for now, and I can see how it would not be too difficult to just code the legend through dash components and make it dynamic.

  3. Layer List - and here I would again like to ask for your help. I am having the same problem encountered here: javascript - How to dynamically set dash-leaflet LayerGroup and MeasureControl properties, and override the same on successive execution? - Stack Overflow - Basically the layer list gets strange after I remove layers in a different order from how I added them. So in the following MRE if you add the three elements and then remove them out of the order you added the layer list gets disordered. Or for example if you try adding layers from the bottom up (polygons, then circle, then markers) the layerlist shows three polygon names. Thank you!

Here’s the MRE:

from dash import Dash, Input, Output
import dash_leaflet as dl
from dash_extensions.enrich import html, DashProxy
import dash_bootstrap_components as dbc



# Some shapes.
markers = dl.Marker(position=[56, 10])
circle = dl.CircleMarker(center=[55, 10])
polygon = dl.Polygon(positions=[[57, 10], [57, 11], [56, 11], [57, 10]])

# Some tile urls.
keys = ["watercolor", "toner", "terrain"]
url_template = "http://{{s}}.tile.stamen.com/{}/{{z}}/{{x}}/{{y}}.png"
attribution = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, ' \
              '<a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a> &mdash; Map data ' \
              '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'

# Create aempy map.
app = Dash()
app.layout = html.Div([dbc.Switch(id="markerswitch", value=False, label="Markers"),
                       dbc.Switch(id="circleswitch", value=False, label="Circle"),
                       dbc.Switch(id="polygonswitch", value=False, label="Polygon"),
                       dl.Map([
                            dl.LayersControl(
                                [dl.BaseLayer(dl.TileLayer(url=url_template.format(key), attribution=attribution),
                                              name=key, checked=key == "toner") for key in keys]
                            , id="layercontrol", collapsed=False)],
                            zoom=7, center=(56, 10))],
                            style={'width': '100%', 'height': '50vh', 'margin': "auto", "display": "block"}
                      )


@app.callback(
    Output("layercontrol", "children"),
    Input("markerswitch", "value"),
    Input("circleswitch", "value"),
    Input("polygonswitch", "value")
)
def update_output_src(markerswitch, circleswitch, polygonswitch):
    baselayers = [dl.BaseLayer(dl.TileLayer(url=url_template.format(key), attribution=attribution),
                                              name=key, checked=key == "toner") for key in keys]

    overlays = []

    if markerswitch is True:
        overlays.append(
            dl.Overlay(dl.LayerGroup(markers), name="markers", checked=True)
        )

    if circleswitch is True:
        overlays.append(
            dl.Overlay(dl.LayerGroup(circle), name="circle", checked=True)
        )

    if polygonswitch is True:
        overlays.append(
            dl.Overlay(dl.LayerGroup(polygon), name="polygon", checked=True)
        )

    finalayers = baselayers + overlays

    return finalayers

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

@dbaboci Thanks! I am happy that you like it :slight_smile:

  1. I was looking into implementing heat maps at some point, but I had trouble finding a plugin that did exactly what I wanted, so I ended up calculating the heatmap manually, and then serving the resulting data as tiles. That being said, if we can figure out the right leaflet plugin to port, I don’t think I will be too much work to add heat map functionality

  2. At the moment, the best (only) solution is to overlay a Div on the map. I am not sure if there is a better solution out there (please let me know, if you find something). It thus require a bit more code than with a plotly chart (but you also get maximum flexibility). Another option would be to include Python helper functions as part of Dash leaflet to address the most common legend cases

  3. The problem is that the React update mechanism is getting confused because the overlays don’t have unique ids. If you assign that, e.g. something like,

    if markerswitch:
        overlays.append(
            dl.Overlay(dl.LayerGroup(markers), name="markers", checked=True, id="markers")
        )
    if circleswitch:
        overlays.append(
            dl.Overlay(dl.LayerGroup(circle), name="circle", checked=True, id="circle")
        )
    if polygonswitch:
        overlays.append(
            dl.Overlay(dl.LayerGroup(polygon), name="polygon", checked=True, id="polygon")
        )

it should work as intended.

1 Like

Works perfectly! Thank you!

Hi @dbaboci,
Hi @Emil,

This thread of messages is previous, its gold. I learner a lot!

I have one question about rendering large amount of data on map. I am developing prototype right now, but in future app should be able to show 4m points on map (not in same moment, but when zoomed in enough, it should be shown). Also, around 1m line strings should be present in prod version.

How rendering is done? Is it filtered on backend side and only fraction of points/lines are sent to frontend, or all data are initially loaded and stored in browser?

Thanks @Emil for creating such a library.
Thanks @dbaboci for starting this thread.

Hi @Milan ,

I believe the marker clutering approach using geojson will work for 4m points, but the amount of data would be so large that the initial load time of the map will become significant (all data are sent in one go). If you (and your users) can live with that, you might be able to use dash-leaflet as-is. I haven’t tried with 1m line strings, but I think it might be too heavy to render - event if you can wait for the data to load.

With the amount of data you need to represent, you would probably need to use a tiling approach where data is sliced and/or resampled by the server, and sent to the client as needed. I have only worked with image-based tiles, but vector tiles is likely more suitable for your usecase. This feature is currently not available in dash-leaflet, but I would be open to look at a PR.

1 Like

Thank you @Emil!

I have some idea for implementation, I need to experiment more with it.

What I have in mind is getting min/max values of current zoom level (lon/lat). Then filtering data on server side, checking if number of data points or line strings is greater than some number (one thousand for example) and sending data to front end. If number of data points that are in ‘current zoom range’ is greater than some specific number (one thousand), then asking user to zoom-in more to see detailed data.

P.S.
Sorry for late response, I haven’t seen notification (even though I received).

Best regards,
Milan

For the filtering approach, I would recommend that you look into existing libraries, rather than implementing the slicing yourself. A quick search indicates multiple libraries addressing the issue, e.g. geojson-vt or VectorGrid.

Thanks, I will take deeper a look at those libraries.

Btw libraries look like they are doing slicing and filtering within browser. I would need similar logic, but on server side.

It’s good place to start searching for suitable solution.

Hi @Milan,

I would also recommend to look at the possibility of serving the data through google earth engine or other similar providers. You can style and filter them through your backend by using the GEE API. In my experience this is more efficient and fast - but you you don’t have frontend interactivity since you are serving tiles.

Best,

Joni