Closed sebastian-schweizer closed 7 months ago
The viewport property was removed to align with the new React-Leaflet API. However, I without it, there is no easy way (without writing JS) to set zoom/center after map initialization - but there should be. I am considering a few options,
1) Make the center
and zoom
properties mutable
(+) is simple, intuitive
(-) if you want to move the map, you may need to target two properties as output (both zoom and center)
2) Re-introduce the viewport property similar to before (-) is not as intuitive (must users first intuition is to target zoom/center props directly) (+) you only need to set one property as output to move the map
3) Do both (+) best of both worlds, maybe? (-) multiple ways of doing the same thing (-) possibly code complexity
Finally, I don't like that there's currently no way to specify how to move the map, i.e. "jump-to" (as before), "pan-to", or "fly-to". With (1) it would be possible to add a property to specify the how, but then you're suddenly targeting 3 props to move the map, and it's suddenly not as intuitive. With (2), an option would be to add the "how" as part of the viewport spec, i.e. something like
{center: number, zoom: number, animate?: "set" | "pan" | "fly", options?: object}
it could even be extended to include bounds also,
{center?: number, zoom?: number, bounds?: number[][], animate?: "set" | "pan" | "fly", options?: object}
so that the map would fit the bounds if provided. This solution would seem to be the most generic, but may also be less beginner friendly. What are your thoughts, @gisensing-sschweizer ?
I like all three options. But I would prefer (2), because with more complex callbacks the amount of output increases, and it is more convenient to only specify one output viewport
. The introduction of a dictionary which also includes the options to pass bounds
and the animation
type would be great! As you have already pointed out, the greatest disadvantages are that it is not very intuitive with the center
and zoom
properties and of course that it would no longer be aligned with React-Leaftlet API in terms of this component. Maybe it would even be useful to replace the center
and zoom
property with an initial viewport
property. In my opinion that would make it more intuitive and generic.
I party also like (3) but I don’t like multiple to do the same. What if you (accidentally) target in one callback the center
and zoom
properties and in another callback the viewport
property? As far as I understand that could lead to inconsistent behavior - if it is technically even possible to implement.
Option (1) would be possibly the easiest way to implement and also be the most beginner-friendly option. However, I find it a pity that the properties bounds
and animation
are not taken into account in this case.
By the way, dash-leaflet and also the new documentation are amazing! Thank you very much :)
I have made a draft implementation of approach (2) and pushed a release candidate that you should be able to test out,
pip install dash-leaflet==1.0.9rc1
Here is a small example app,
import dash_leaflet as dl
from dash_extensions.enrich import DashProxy, html, Input, Output, MultiplexerTransform
app = DashProxy(prevent_initial_callbacks=True, transforms=[MultiplexerTransform()])
app.layout = html.Div([
dl.Map([
dl.TileLayer()
], center=[56, 10], zoom=6, style={'height': '50vh'}, id="map"),
html.Button("Fly", id="fly"),
])
@app.callback(Output("map", "viewport"), Input("fly", "n_clicks"))
def fly_to(_):
return dict(center=[40, -98], zoom=5, transition="flyTo")
if __name__ == '__main__':
app.run_server()
Hi, in the previous version I was able to get the bounds of the map using a callback function and filtering my data according to the map bounds:
@app.callback(
Output('histogram', 'figure'),
Input('map', 'bounds'),
)
def update_histogram(bounds):
print(bounds)
# Your existing code to update the histogram based on map bounds
df_filtered = df[(df['latitude'] >= bounds[0][0]) & (df['latitude'] <= bounds[1][0]) &
(df['longitude'] >= bounds[0][1]) & (df['longitude'] <= bounds[1][1])]
#... (rest of code)
return fig
How can I achieve the same results with the new version?
@lperozzi That should work similar to before. I just tested the following code,
from dash import Dash, html, Output, Input
import dash_leaflet as dl
import json
app = Dash()
app.layout = html.Div([
dl.Map(dl.TileLayer(), center=[56, 10], zoom=6, style={'height': '50vh'}, id="map"),
html.Div(id="log")
])
@app.callback(Output("log", "children"), Input("map", "bounds"))
def update(bounds):
return json.dumps(bounds)
if __name__ == '__main__':
app.run_server()
which seems to work as intended with version 1.0.9rc1
. What version are you using?
Thank you very much for implementing approach (2)! Everything works for me with version 1.0.9rc1. I really like how animations like flyTo
are now supported.
Despite having set the property trackViewport
to True in the MapContainer component the variables current_viewport
, current_center
and current_zoom
are always None in the following example and cannot trigger the callback. Is there a way that the viewport could be used to trigger a callback?
Basically, I try to implement a location search. The geocoder provides a viewport and a location and then the map should fly to this viewport and highlight the location with a marker. But if the user zooms or pans the highlighting marker should disappear. Conceptually, this mechanism should work with the following callback:
import dash_leaflet as dl
from dash_extensions.enrich import DashProxy, html, Input, Output, State, callback, callback_context, MultiplexerTransform
app = DashProxy(prevent_initial_callbacks=True, transforms=[MultiplexerTransform()])
app.layout = html.Div([
dl.Map([
dl.TileLayer(),
dl.Pane(
id='marker-pane',
name='marker-pane',
children=[]
)
], center=[56, 10], zoom=6, style={'height': '50vh'}, id="map", trackViewport=True),
html.Button("Fly", id="fly")
])
@callback(
Output('map', 'viewport'),
Output('marker-pane', 'children'),
Input('fly', 'n_clicks'),
Input('map', 'viewport'),
State('map', 'center'),
State('map', 'zoom')
)
def track_viewport(click, current_viewport, current_center, current_zoom):
if callback_context.triggered[0]['prop_id'] == 'fly.n_clicks':
# Fly to new viewport and show a marker
return dict(center=[40, -98], zoom=5, transition="flyTo"), dl.Marker(position=[40, -98])
else:
# Otherwise remove the marker
return dash.no_update, []
if __name__ == '__main__':
app.run_server()
There was a bug with respect to the zoom/center part of the viewport tracking. It should be fixed in 1.0.9rc2
. With this release, you should be able to move the marker with the viewport with code like,
import dash_leaflet as dl
from dash_extensions.enrich import DashProxy, html, Input, Output, callback
app = DashProxy(prevent_initial_callbacks=True)
app.layout = html.Div([
dl.Map([
dl.TileLayer(),
dl.LayerGroup(id='group')
], center=[56, 10], zoom=6, style={'height': '50vh'}, id="map"),
html.Button("Fly", id="fly")
])
@callback(
Output('map', 'viewport'),
Input('fly', 'n_clicks'),
)
def change_viewport(n_clicks):
return dict(center=[40, -98], zoom=5, transition="flyTo")
@callback(
Output('group', 'children'),
Input('map', 'center')
)
def track_viewport(center):
return dl.Marker(position=center)
if __name__ == '__main__':
app.run_server()
Now, the zoom and center properties trigger a callback when the viewport changes. But it is a pity that the animation flyTo triggers a callback multiple times that has the property zoom or center as input. It would be very useful if the property viewport was also able to trigger a callback. If that was the case, I could set a new viewport with the animation flyTo and the property viewport
would hold the target viewport as long as the user does no panning or zooming. If the user carries out some panning or zooming the property viewport would change the center and zoom value. Would that make sense?
Thank you for implementing all these things!
@gisensing-sschweizer I have pushed a (non-rc) 1.0.9 release with the changes discussed here. I like the current approach of using the viewport
prop only to manipulate the map view, while other props (zoom
, center
, bounds
) remain read-only.
I do see the issue though - and I suspect it may be a bug in Leaflet (similar to this one). I'll try to think of a good solution; maybe a debounce
would help (i.e. not updating the viewport if it changes too fast; that is, the map is in motion).
Apparently in dash-leaflet version 1.0 there is no longer the viewport property for a MapContainer. In version 0.1 it was possible to set a viewport as a dictionary containing zoom level and center coordinates. Is there a way to change the viewport via a callback with the bounds property?
Thanks!