-
Notifications
You must be signed in to change notification settings - Fork 7
Description
Hi! Thank you for the work you've already done on shinywidgets. I have a question related to ipyleaflet within shinywidgets. The following example is an adaptation of the map-distance template, aiming to update the map without needing to re-render it from scratch. When the app starts, everything works as expected, but strange behaviors begin to appear upon interaction. Let's go through them one by one:
- When the app is launched, the layer selection button in the top right corner shows three layers in addition to the basemaps. The issue arises when a city is selected in either "Location 1" or "Location 2". The map with the two points and the connecting line is rendered, but the "line" layer disappears on the layer button. Also, note that the layer button switches position with the search button.
- Building on the previous item, when clicking "Sample point", no point appears on the map and the layer button disappears. If a city is selected in "Location 1" or "Location 2", the sampled point appears. Clicking "Sample point" again causes the point to disappear once more.
- When the app is launched and "Sample point" is clicked, the sampled point does not appear on the map and the layers button disappears. The point in the map only shows up if a city in "Location 1" or "Location 2" is changed.
When running the code that builds the map, adds and removes points and the line using Positron IDE, everything works as expected. I've been trying to get things working for a while now, but to no avail. This includes adding marker groups with ipyleaflet and other approaches with the leafmap package.
import ipyleaflet as L
from shiny import App, reactive, ui
from shinywidgets import output_widget, render_widget
from numpy import random
CITIES = {
"New York": {"latitude": 40.7128, "longitude": -74.0060, "altitude": 33},
"London": {"latitude": 51.5074, "longitude": -0.1278, "altitude": 36},
"Paris": {"latitude": 48.8566, "longitude": 2.3522, "altitude": 35},
"Tokyo": {"latitude": 35.6895, "longitude": 139.6917, "altitude": 44},
"Sydney": {"latitude": -33.8688, "longitude": 151.2093, "altitude": 39},
"Los Angeles": {"latitude": 34.0522, "longitude": -118.2437, "altitude": 71},
"Berlin": {"latitude": 52.5200, "longitude": 13.4050, "altitude": 34},
"Rome": {"latitude": 41.9028, "longitude": 12.4964, "altitude": 21},
"Beijing": {"latitude": 39.9042, "longitude": 116.4074, "altitude": 44},
"Moscow": {"latitude": 55.7558, "longitude": 37.6176, "altitude": 156},
"Cairo": {"latitude": 30.0444, "longitude": 31.2357, "altitude": 23},
"Rio de Janeiro": {"latitude": -22.9068, "longitude": -43.1729, "altitude": 8},
"Toronto": {"latitude": 43.6511, "longitude": -79.3832, "altitude": 76},
"Dubai": {"latitude": 25.2769, "longitude": 55.2963, "altitude": 52},
"Mumbai": {"latitude": 19.0760, "longitude": 72.8777, "altitude": 14},
"Seoul": {"latitude": 37.5665, "longitude": 126.9780, "altitude": 38},
"Madrid": {"latitude": 40.4168, "longitude": -3.7038, "altitude": 667},
"Amsterdam": {"latitude": 52.3676, "longitude": 4.9041, "altitude": -2},
"Buenos Aires": {"latitude": -34.6037, "longitude": -58.3816, "altitude": 25},
"Stockholm": {"latitude": 59.3293, "longitude": 18.0686, "altitude": 14},
"Boulder": {"latitude": 40.0150, "longitude": -105.2705, "altitude": 1634},
"Lhasa": {"latitude": 29.6500, "longitude": 91.1000, "altitude": 3650},
"Khatmandu": {"latitude": 27.7172, "longitude": 85.3240, "altitude": 1400},
}
city_names = sorted(list(CITIES.keys()))
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_selectize(
"loc1", "Location 1", choices=city_names, selected="New York"
),
ui.input_selectize("loc2", "Location 2", choices=city_names, selected="London"),
ui.input_action_button("sample", "Sample point")
),
output_widget("map"),
title="Location Distance Calculator",
fillable=True,
class_="bslib-page-dashboard",
)
def server(input, output, session):
# Reactive values to store location information
loc1 = reactive.value()
loc2 = reactive.value()
locpoint = reactive.value()
# Update the reactive values when the selectize inputs change
@reactive.effect
def _():
loc1.set(CITIES.get(input.loc1()))
loc2.set(CITIES.get(input.loc2()))
@reactive.effect
@reactive.event(input.sample)
def _():
city = random.choice(list(CITIES.keys()), 1)[0]
locpoint.set(CITIES.get(city))
# Convenient way to get the lat/lons as a tuple
@reactive.calc
def loc1xy():
return loc1()["latitude"], loc1()["longitude"]
@reactive.calc
def loc2xy():
return loc2()["latitude"], loc2()["longitude"]
@reactive.calc
def locpointxy():
return locpoint()["latitude"], locpoint()["longitude"]
# For performance, render the map once and then perform partial updates
# via reactive side-effects
@render_widget
def map():
esri_sat = L.basemap_to_tiles(L.basemaps.Esri.WorldImagery)
esri_sat.name = "Esri.WorldImagery"
esri_sat.base = True
osm = L.basemap_to_tiles(L.basemaps.OpenStreetMap.Mapnik)
osm.name = "OpenStreetMap"
osm.base = True
m = L.Map(zoom=2, center=(0, 0), scroll_wheel_zoom=True, zoom_control = False, layers=[esri_sat, osm])
m.add(L.ZoomControl(position='topright'))
m.add(L.LayersControl(position='topleft'))
search_control = L.SearchControl(
position="topleft",
url='https://nominatim.openstreetmap.org/search?format=json&q={s}',
zoom=10,
auto_collapse = True
)
m.add(search_control)
return m
# Add marker for first location
@reactive.effect
def _():
update_marker(map.widget, loc1xy(), "loc1")
# Add marker for second location
@reactive.effect
def _():
update_marker(map.widget, loc2xy(), "loc2")
# Add marker for sample location
@reactive.effect
def _():
update_marker(map.widget, locpointxy(), "point")
# Add line between first and second location
@reactive.effect
def _():
update_line(map.widget, loc1xy(), loc2xy())
def update_marker(map: L.Map, loc: tuple, name: str):
remove_layer(map, name)
map.add(L.Marker(location=loc, draggable=False, name=name, title = name))
# Trying map.substitute() instead of map.remove() and map.add()
# m = L.Marker(location=loc, draggable=False, name=name)
# is_new_layer = True
# for layer in map.layers:
# if layer.name == name:
# map.substitute(layer, m)
# is_new_layer = False
# break
# if is_new_layer:
# map.add(m)
def update_line(map: L.Map, loc1: tuple, loc2: tuple):
remove_layer(map, "line")
map.add(
L.Polyline(locations=[loc1, loc2], color="blue", weight=2, name="line")
)
# Trying map.substitute() instead of map.remove() and map.add()
# name="line"
# is_new_layer = True
# for layer in map.layers:
# if layer.name == name:
# map.substitute(layer, L.Polyline(locations=[loc1, loc2], color="blue", weight=2, name="line"))
# is_new_layer = False
# break
# if is_new_layer:
# map.add(m)
def remove_layer(map: L.Map, name: str):
for layer in map.layers:
if layer.name == name:
map.remove(layer)
app = App(app_ui, server)
I am running the app on Ubuntu 24.04.3 LTS using Python 3.13.7 and the following packages.
anywidget==0.9.18
appdirs==1.4.4
asgiref==3.9.1
asttokens==3.0.0
branca==0.8.1
click==8.2.1
comm==0.2.3
decorator==5.2.1
executing==2.2.0
h11==0.16.0
htmltools==0.6.0
idna==3.10
ipyleaflet==0.20.0
ipython==9.4.0
ipython_pygments_lexers==1.1.1
ipywidgets==8.1.7
jedi==0.19.2
Jinja2==3.1.6
jupyter-leaflet==0.20.0
jupyter_core==5.8.1
jupyterlab_widgets==3.0.15
linkify-it-py==2.0.3
markdown-it-py==4.0.0
MarkupSafe==3.0.2
matplotlib-inline==0.1.7
mdit-py-plugins==0.5.0
mdurl==0.1.2
narwhals==2.1.2
numpy==2.3.2
orjson==3.11.2
packaging==25.0
parso==0.8.4
pexpect==4.9.0
platformdirs==4.3.8
prompt_toolkit==3.0.51
psygnal==0.14.1
ptyprocess==0.7.0
pure_eval==0.2.3
Pygments==2.19.2
python-dateutil==2.9.0.post0
python-multipart==0.0.20
questionary==2.1.0
setuptools==80.9.0
shiny==1.4.0
shinywidgets==0.7.0
six==1.17.0
sniffio==1.3.1
stack-data==0.6.3
starlette==0.47.2
traitlets==5.14.3
traittypes==0.2.1
typing_extensions==4.14.1
uc-micro-py==1.0.3
uvicorn==0.35.0
watchfiles==1.1.0
wcwidth==0.2.13
websockets==15.0.1
widgetsnbextension==4.0.14
xyzservices==2025.4.0