I am working on a project to visualise weather forecasts using Shiny for Python and Folium. The objective is to allow users to interact with a slider to select the forecasted time, and display the corresponding weather data on a map.
The problem I'm encountering: Every time the user changes the forecasted time using the slider, the map resets to the initial zoom level and center position. Ideally, I would like the map to retain the current zoom and center, even when switching the displayed layer (i.e., forecasted time).
I’ve tried to implement custom JavaScript in the app to capture the zoom and center changes and reapply them after the slider update, but it doesn’t seem to work as expected. The zoom and center are always reset to the initial values defined when the map is first created. Upon inspecting the web app’s source code, it looks like my JavaScript might not be correctly integrated with the rest of the application.
My setup: Shiny for Python for the web app structure. Folium for rendering the map and adding the weather data as layers. A slider for selecting different forecasted time steps. Attempted to use JavaScript to capture and restore the map's view settings (zoom level and center) when changing the slider.
Question: Is it possible to retain the zoom level and map center across updates when changing the forecast time using my current stack (probably similar to In shiny, how to fix (lock) leaflet map view zoom and center?, just in Python and not in R)? If so, how can I properly include JavaScript to achieve this?
This is the shortest version still exhibiting the problem:
from shiny import App, render, ui
import folium
# Define the Shiny UI
app_ui = ui.page_fluid(
ui.h2("Dynamic Folium Map with Layer Switching"),
ui.input_slider("layer_slider", "Choose Layer (1 = Marker, 2 = Circle)", min=1, max=2, value=1),
ui.output_text_verbatim("slider_value"),
ui.output_ui("map") # Output container for the map
)
# Define the Shiny server
def server(input, output, session):
# Display the slider value
@output
@render.text
def slider_value():
return f"Layer selected: {input.layer_slider()}"
# Generate and display the map dynamically
@output
@render.ui
def map():
# Create a base map centered on Germany
m = folium.Map(location=[51.1657, 10.4515], zoom_start=6)
# Add layers depending on the slider value
if input.layer_slider() == 1:
folium.Marker([51.1657, 10.4515], popup="Marker in Germany").add_to(m)
elif input.layer_slider() == 2:
folium.Circle([51.1657, 10.4515], radius=50000, color='blue', popup="Circle Layer").add_to(m)
# Return the map as an HTML string
map_html = m._repr_html_()
# Add custom JavaScript to preserve zoom level and map center
custom_js = """
<script>
let mapZoom = 6;
let mapCenter = [51.1657, 10.4515];
// Function to capture map's zoom and center
function captureMapState() {
mapZoom = map.getZoom();
mapCenter = map.getCenter();
}
// Function to restore map's zoom and center
function restoreMapState() {
map.setView(mapCenter, mapZoom);
}
// Capture state after zoom or move events
map.on('zoomend', captureMapState);
map.on('moveend', captureMapState);
// Restore the view after the map is updated
document.addEventListener('DOMContentLoaded', function() {
restoreMapState();
});
</script>
"""
# Embed the map and attach the custom JavaScript
return ui.HTML(f"""
<div style="width:100%; height:500px;">
{map_html}
</div>
{custom_js}
""")
# Create the Shiny app
app = App(app_ui, server)
# Run the app
app.run(port=8000)
I would recommend using ipyleaflet
instead of folium
since it is an ipywidget
and hence supports methods to update the map object post creation.
Here is editable code in Shinylive
from ipyleaflet import Map, Marker, Circle
from shiny.express import ui, input
from shinywidgets import render_widget
from shiny import reactive, render
def remove_all_layers(m):
for i in range(1, len(m.layers)):
m.remove_layer(m.layers[1])
ui.h2("An ipyleaflet Map")
ui.input_slider('layer_slider', 'Layer Slider', 0, 3, 0, step=1)
@render_widget # <<
def map():
center = (51.1657, 10.4515)
m = Map(center=center, zoom=6)
# marker = Marker(location=center)
# m.add(marker)
return m
@reactive.effect
def add_layer():
m = map.widget
remove_all_layers(m)
if input.layer_slider() == 1:
marker = Marker(
location=[51.1657, 10.4515],
title="Marker in Germany"
)
m.add(marker)
elif input.layer_slider() == 2:
circle = Circle(
location=[51.1657, 10.4515],
radius=50000,
color='blue',
title="Circle Layer"
)
m.add(circle)